映射类型(Mapped Types)
映射类型的本质
映射类型是 TypeScript 类型系统中最强大的元编程工具之一。它让你可以基于已有类型机械式地推导出新类型——不需要手写每个属性,而是用一个"模板公式"遍历所有属性并变换。
基本语法
映射类型允许你基于已有类型创建新类型,通过遍历已有类型的每个属性,对属性类型进行变换。语法是 { [K in keyof T]: ... }。
type User = { id: number; name: string; email: string };
// 手动实现 Partial(所有属性变可选)
type MyPartial<T> = {
[K in keyof T]?: T[K]; // ? 使属性可选
};
// 手动实现 Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 移除可选(Required 的实现)
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? 移除可选
};
// 实用:将所有值类型变为 Promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
type AsyncUser = Promisify<User>;
// { id: Promise<number>; name: Promise<string>; email: Promise<string> }
键名重映射(Key Remapping)
// as 子句:对键名进行变换
type Getters<T> = {
// 将每个属性名 K 变换为 getK(如 name → getName)
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }
// 过滤属性:as 子句返回 never 时该属性被排除
type OmitNever<T> = {
// 只保留值类型不为 never 的属性
[K in keyof T as T[K] extends never ? never : K]: T[K];
};
// 只保留 string 类型的属性(过滤掉 number/boolean 等)
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringProperties<User>;
// { name: string; email: string }(去掉了 id: number)
条件类型(Conditional Types)
条件类型的原理
条件类型的语法是 T extends U ? A : B,含义是:如果类型 T 可以赋值给类型 U,则结果为 A,否则为 B。它本质上是类型系统中的"三元运算符",让你可以根据类型间的关系动态计算出结果类型。
当 T 是裸类型参数(naked type parameter,直接写 T 而不是 T[]、[T]、Promise<T> 等包装形式)且传入联合类型时,条件类型会自动分配:TypeScript 将联合的每个成员分别代入求值,再把结果联合起来。
示例:ToArray<string | number> 等同于 ToArray<string> | ToArray<number> = string[] | number[],而不是 (string | number)[]。
如果你不想要分配行为,把 T 包裹起来:[T] extends [U] ? A : B,这样联合类型整体参与比较,不会分配。
// 基本语法:T extends U ? A : B
type IsArray<T> = T extends any[] ? true : false;
type R1 = IsArray<string[]>; // true
type R2 = IsArray<string>; // false
// NonNullable 的实现:排除 null 和 undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;
// 利用分配律:MyNonNullable<string | null | undefined>
// = (string extends null|undefined ? never : string)
// | (null extends null|undefined ? never : null)
// | (undefined extends null|undefined ? never : undefined)
// = string | never | never = string
// 分配式条件类型(Distributive)
// 当 T 是联合类型时,条件类型会分别对每个成员求值再联合
type ToArray<T> = T extends any ? T[] : never;
type R3 = ToArray<string | number>; // string[] | number[]
// 禁用分配:用元组包裹,让整个联合参与比较
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R4 = ToArrayNonDist<string | number>; // (string | number)[]
// 实用工具:从联合类型中提取/排除成员(利用 never 过滤)
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type Strings = Extract<string | number | boolean, string>; // string
type NonStrings = Exclude<string | number | boolean, string>; // number | boolean
infer:在条件类型中推断类型
infer 关键字只能在条件类型的 extends 子句中使用。它告诉 TypeScript:"我不知道这个位置的类型,请你帮我推断,并把推断结果绑定到这个类型变量上供我使用"。这让条件类型从一个"布尔判断"变成了"类型提取器"。
// 提取函数返回类型(ReturnType 的手动实现)
// 读法:如果 T 是一个函数类型(参数任意),推断其返回类型为 R,结果就是 R
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R5 = MyReturnType<() => string>; // string
type R6 = MyReturnType<(x: number) => boolean>; // boolean
// 提取函数参数类型(Parameters 的实现)
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = MyParameters<(a: string, b: number) => void>;
// [a: string, b: number]
// 提取 Promise 的值类型(Awaited 的实现原型)
// 若 T 是 Promise<V>,推断 V;否则 T 原样返回
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type R7 = Unwrap<Promise<string>>; // string
type R8 = Unwrap<number>; // number(不是 Promise,原样返回)
// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type R9 = ElementType<string[]>; // string
type R10 = ElementType<User[]>; // User
// 实战:提取构造函数的实例类型(InstanceType 的实现)
type MyInstanceType<T> = T extends new (...args: any[]) => infer I ? I : never;
class Animal { name = ''; }
type AnimalInstance = MyInstanceType<typeof Animal>; // Animal
模板字面量类型(Template Literal Types)
TypeScript 4.1 引入的模板字面量类型让你可以在类型层面做字符串拼接和变换。它的语法和 JavaScript 的模板字符串完全相同,但操作的是类型而不是值。当模板中包含联合类型时,TypeScript 会自动做笛卡尔积展开,生成所有可能的字符串字面量类型。
// TypeScript 4.1+ 特性
type EventName = `on${Capitalize}` ;
// 'onClick' | 'onChange' | ... (无限集合)
// 实际应用:生成 CSS 属性类型
type Side = 'top' | 'bottom' | 'left' | 'right';
type Padding = `padding-${Side}`;
// 'padding-top' | 'padding-bottom' | 'padding-left' | 'padding-right'
// 生成事件处理器类型
type HTMLTag = 'div' | 'span' | 'button';
type EventHandlers = {
[K in `on${Capitalize}Click` ]: () => void;
};
// { onDivClick: () => void; onSpanClick: () => void; onButtonClick: () => void }
// 类型安全的 i18n key
type Lang = 'en' | 'zh' | 'ja';
type Namespace = 'common' | 'auth' | 'dashboard';
// 笛卡尔积:3 × 3 = 9 个字面量类型
type TranslationKey = `${Lang}.${Namespace}`;
// 'en.common' | 'en.auth' | 'en.dashboard' | 'zh.common' | ...
内置字符串操纵类型
TypeScript 内置了 4 个专门用于模板字面量的字符串变换工具类型:
// 实战:从对象类型自动生成事件类型
type ModelEvents<T> = {
// on + 属性名首字母大写 + Change → onChange 事件回调
[K in keyof T as `on${Capitalize<string & K>}Change`]:
(newVal: T[K], oldVal: T[K]) => void;
};
type UserModel = { name: string; age: number; active: boolean };
type UserEvents = ModelEvents<UserModel>;
// {
// onNameChange: (newVal: string, oldVal: string) => void;
// onAgeChange: (newVal: number, oldVal: number) => void;
// onActiveChange: (newVal: boolean, oldVal: boolean) => void;
// }
// 实战:类型安全的嵌套路径(点语法访问)
type DotPath<T, K extends keyof T = keyof T> =
K extends string
? T[K] extends object
? K | `${K}.${DotPath<T[K]>}`
: K
: never;
type Config = { server: { host: string; port: number }; debug: boolean };
type ConfigPath = DotPath<Config>;
// 'server' | 'debug' | 'server.host' | 'server.port'
这些高级类型特性不是「炫技」——它们在大型项目中有实际价值:映射类型让 API 类型自动衍生(如从数据库模型生成 HTTP 请求/响应类型),模板字面量类型让路由系统和表单字段名称类型安全,条件类型让工具函数的返回类型精确匹配输入类型。掌握这些工具,可以消除大量手写类型标注的冗余工作。
组合应用:实战工具类型
真正的威力来自将映射类型、条件类型、infer 和模板字面量类型组合使用:
// DeepReadonly:递归将所有嵌套属性变为只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]> // 递归处理嵌套对象
: T[K]; // 基础类型不变
};
// DeepPartial:递归将所有嵌套属性变为可选
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// ValueOf:提取对象类型的所有值类型组成联合类型
type ValueOf<T> = T[keyof T];
const STATUS = { ACTIVE: 'active', INACTIVE: 'inactive' } as const;
type StatusValue = ValueOf<typeof STATUS>; // 'active' | 'inactive'
// FunctionProperties:提取对象中所有函数类型的属性
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T]; // 末尾 [keyof T] 是索引访问,将对象类型的所有值联合起来
type Service = {
name: string;
start(): void;
stop(): void;
port: number;
};
type ServiceMethods = FunctionPropertyNames<Service>; // 'start' | 'stop'
本章小结
- 映射类型 [K in keyof T]:遍历 T 的所有属性并变换;修饰符 +/-?、+/-readonly 控制可选/只读;as 子句(TS 4.1+)支持键名重映射和属性过滤(返回 never 则排除该属性)。
- 条件类型 T extends U ? A : B:类型系统中的三元运算符;当 T 是裸类型参数时对联合类型自动分配(Distributive);用 [T] extends [U] 包裹可禁用分配行为。
- infer 关键字:只能在条件类型的 extends 子句中使用;作用是"占位推断"——让 TypeScript 在匹配时自动推断该位置的类型并绑定到变量。是 ReturnType/Parameters/InstanceType 等内置工具类型的实现基础。
- 模板字面量类型(TS 4.1+):在类型层面做字符串拼接;联合类型参与拼接时会自动展开为笛卡尔积;配合 Capitalize/Uppercase 等内置工具可生成属性名变换。
- 组合应用:DeepReadonly/DeepPartial 等递归工具类型、FunctionPropertyNames 等属性筛选类型,展示了高级类型在实际项目中消除重复代码的能力。