Skip to content
条件类型、映射类型与 infer
条件类型
条件类型为类型系统带来了分支能力。其语法为 T extends U ? X : Y,当类型 T 可赋值给 U 时得到 X 分支,否则得到 Y 分支。
条件类型常与泛型组合,在类型层面根据输入类型决定输出类型。例如一个函数既可以接收 number 也可以接收 string,返回的标签结构取决于参数类型。以往需要靠重载实现,现在一个条件类型加一个泛型签名即可描述这种关系。
ts
type NameOrId<T extends number | string> = T extends number
? { id: T }
: { name: T };
function createLabel<T extends number | string>(value: T): NameOrId<T> {
// 运行时仍需要类型守卫进行区分
if (typeof value === 'number') {
return { id: value } as any;
}
return { name: value } as any;
}
const a = createLabel(100); // { id: number }
const b = createLabel("ts"); // { name: string }注意,条件类型只作用于编译阶段的类型推断,运行时代码仍须通过 typeof 之类的检查去区分实际值,因此示例中使用了 as any 绕过类型检查。
分配律与元组阻断
分布式条件类型
当条件类型的 extends 左侧是裸类型参数,并且实际传入的是一个联合类型时,TypeScript 会自动将条件类型分发给联合的每个成员,分别求值后再合并为联合。
ts
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]ToArray<string | number> 没有产出 (string | number)[],而是先将 string | number 拆成 string 与 number,各自计算 ToArray<string> → string[]、ToArray<number> → number[],最后合并为 string[] | number[]。许多过滤、映射型的工具类型正是依赖这一行为。
避免分配
如果希望将联合类型当作整体进行判断,可以用方括号把 extends 两侧括起来,此时条件类型不再触发分配。
ts
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>; // (string | number)[]原理在于 [T] 不再是裸类型参数,而分配律只对直接出现在 extends 左侧的类型参数生效。
infer 类型推断
infer 只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。它的作用是把匹配到的结构中某个部分提取出来。
提取数组元素类型
ts
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number当 Flatten 的实参是数组时,infer Item 会捕获数组的元素类型,从而在 true 分支中返回该元素类型;否则原样返回。
提取函数返回类型与参数
内置的 ReturnType 就基于这一模式实现:
ts
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type Fn = (x: number, y: string) => boolean;
type R = MyReturnType<Fn>; // boolean类似地可以提取参数元组:
ts
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type P = MyParameters<(a: string, b: number) => void>; // [string, number]对重载函数的限制
当从拥有多个调用签名的类型(例如重载函数)推断时,infer 只会使用最后一个签名。因此如果各个重载的返回类型不同,ReturnType 拿到的可能并不是期望的类型。
多个 infer 与联合推断
同一条件类型中可以出现多个 infer。如果一个推断变量在协变位置(如返回值、数组元素)多次出现,TypeScript 会将这些候选取联合。
ts
type UnionOfReturns<T> = T extends ((...args: any[]) => infer R)[] ? R : never;
type U = UnionOfReturns<[() => string, () => number]>; // string | number嵌套条件类型:Unpacked
通过串联多个条件类型可以逐层穿透嵌套结构。下面的 Unpacked 依次尝试匹配数组、函数、Promise,最后原样返回。
ts
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => boolean>; // boolean
type T3 = Unpacked<Promise<number>>; // number
type T4 = Unpacked<number>; // number索引访问类型与条件类型配合
索引访问类型 T[K] 是类型层面的属性访问,当 K 为联合类型时,结果为对应属性类型的联合。
ts
interface Person {
name: string;
age: number;
}
type NameOrAge = Person['name' | 'age']; // string | number将该特性与映射类型、条件类型组合,可以筛选出特定属性名。例如选出所有函数类型的属性名:
ts
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
interface Part {
id: number;
name: string;
update(): void;
setPart(newName: string): void;
}
type FuncNames = FunctionPropertyNames<Part>; // "update" | "setPart"这里首先用映射类型构造了一个对象类型,其值在函数属性处为原属性名,其余为 never。随后通过 [keyof T] 索引取出所有值,never 在联合中会被自动消除,最终得到仅含函数属性名的联合类型。
映射类型进阶:修饰符与键重映射
修饰符
映射类型通过 + / - 前缀来增减 readonly 和 ? 修饰符。
ts
// 全部变为只读
type MyReadonly<T> = { readonly [P in keyof T]: T[P] };
// 移除只读
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
// 全部变为可选
type MyPartial<T> = { [P in keyof T]?: T[P] };
// 移除可选(变为必填)
type Required<T> = { [P in keyof T]-?: T[P] };不加前缀的 readonly 或 ? 等同于加上 +,默认映射类型不会改写原类型的修饰符。
键重映射(as 子句)
TypeScript 4.1 起支持在映射类型中使用 as 子句基于原键名构造新键名。
ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }string & K 确保 TypeScript 在重映射时将键名视为字符串字面量,以便 Capitalize 等内建类型正确处理。
递归条件类型与深层变换
将条件类型、映射类型与递归组合,可以实现 DeepPartial 这类逐层应用的工具类型。
ts
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};递归的终止条件为当属性类型不再满足 object 时停止展开,直接使用原类型。同理可以得到 DeepRequired,利用 -? 移除可选性并对嵌套对象递归:
ts
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};条件类型本身不允许直接递归引用自身,例如 type ElementType<T> = T extends any[] ? ElementType<T[number]> : T 会直接报错。但像 DeepPartial 这样在映射类型的值位置递归引用是允许的,因为递归并未直接作用于条件类型自身。
即便如此,TypeScript 编译器对类型实例化存在递归深度限制(默认约 50 层),超出时会报错 Type instantiation is excessively deep and possibly infinite。编写深层变换类型时,必须确保递归路径有明确终止条件,避免对无限嵌套结构展开。
never 陷阱
never 在联合类型中有特殊的表现。
分配至 never 消失
当以 never 直接传入分配条件类型时,分发不会发生,整个结果直接为 never:
ts
type TestNever<T> = T extends string ? 'yes' : 'no';
type R1 = TestNever<never>; // never利用分配与 never 剔除成员
这一特性被用于从联合类型中剔除成员。内建的 Exclude<T, U> 就依赖此行为:
ts
type Diff<T, U> = T extends U ? never : T;
type Result = Diff<'a' | 'b' | 'c', 'a' | 'd'>; // "b" | "c"流程是:T 先分配为 'a'、'b'、'c',分别与 U 比较,匹配的成员返回 never,不匹配的原样返回。最终联合中的 never 会被 TypeScript 自动消除,留下 "b" | "c"。
在映射类型中,若属性值被计算为 never,该属性同样会被过滤——前述 FunctionPropertyNames 正是运用了这一规则。编写复杂工具类型时须注意 never “在联合中消失”的性质,否则可能无意中丢失类型分支。
