条件类型
问题
什么是 TypeScript 条件类型?如何使用 extends 和 infer 进行类型判断和推断?
答案
条件类型允许根据类型关系选择不同的类型结果,语法类似三元表达式:T extends U ? X : Y。配合 infer 关键字可以在条件分支中推断类型。
基础语法
// 基本形式
type Result = T extends U ? X : Y;
// 如果 T 可赋值给 U,则结果为 X,否则为 Y
// 简单示例
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true(字面量也是 string)
// 多层嵌套
type TypeName<T> = T extends string
? 'string'
: T extends number
? 'number'
: T extends boolean
? 'boolean'
: T extends undefined
? 'undefined'
: T extends Function
? 'function'
: 'object';
type T1 = TypeName<string>; // 'string'
type T2 = TypeName<() => void>; // 'function'
type T3 = TypeName<string[]>; // 'object'
类型分发(Distributive)
条件类型在联合类型上会自动分发:
type ToArray<T> = T extends any ? T[] : never;
// 分发过程
type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// string[] | number[]
// 不是 (string | number)[]
// 实际应用
type Exclude<T, U> = T extends U ? never : T;
type Filtered = Exclude<'a' | 'b' | 'c', 'a'>;
// Exclude<'a', 'a'> | Exclude<'b', 'a'> | Exclude<'c', 'a'>
// never | 'b' | 'c'
// 'b' | 'c'
阻止分发
使用元组包裹防止分发:
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNoDistribute<string | number>;
// (string | number)[]
// 为什么要阻止?
// 判断是否为联合类型
type IsUnion<T, Copy = T> = T extends any
? [Copy] extends [T]
? false
: true
: never;
type Test1 = IsUnion<string>; // false
type Test2 = IsUnion<string | number>; // true
infer 关键字
infer 在条件类型的 extends 子句中声明待推断的类型变量:
推断函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
// 推断参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Fn2 = (a: string, b: number) => void;
type Params = Parameters<Fn2>; // [a: string, b: number]
推断数组元素类型
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Elem = ArrayElement<string[]>; // string
type Elem2 = ArrayElement<[1, 2, 3]>; // 1 | 2 | 3
// 获取数组第一个元素类型
type First<T> = T extends [infer F, ...any[]] ? F : never;
type F1 = First<[1, 2, 3]>; // 1
type F2 = First<string[]>; // string
type F3 = First<[]>; // never
推断 Promise 内部类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A1 = Awaited<Promise<string>>; // string
type A2 = Awaited<Promise<Promise<number>>>; // number(递归)
type A3 = Awaited<string>; // string
推断对象属性类型
type GetProperty<T, K> = K extends keyof T ? T[K] : never;
interface User {
name: string;
age: number;
}
type Name = GetProperty<User, 'name'>; // string
type Unknown = GetProperty<User, 'unknown'>; // never
高级 infer 技巧
多个 infer 位置
// 推断函数的 this 类型
type ThisType<T> = T extends (this: infer U, ...args: any[]) => any
? U
: never;
function greet(this: { name: string }, msg: string) {
return `${msg}, ${this.name}`;
}
type This = ThisType<typeof greet>; // { name: string }
// 同时推断多个类型
type FunctionInfo<T> = T extends (
this: infer This,
...args: infer Args
) => infer Return
? { this: This; args: Args; return: Return }
: never;
字符串模板推断
// 解析路由参数
type ParseParam<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ParseParam<Rest>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ParseParam<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'
// 驼峰转连字符
type CamelToKebab<S extends string> = S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `-${Lowercase<First>}${CamelToKebab<Rest>}`
: `${First}${CamelToKebab<Rest>}`
: S;
type Kebab = CamelToKebab<'backgroundColor'>; // 'background-color'
元组操作
// 获取最后一个元素
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type L = Last<[1, 2, 3]>; // 3
// 移除最后一个元素
type Pop<T extends any[]> = T extends [...infer Rest, any] ? Rest : never;
type Popped = Pop<[1, 2, 3]>; // [1, 2]
// 翻转元组
type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]
条件类型应用
类型过滤
// 从对象中提取函数属性
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
interface Example {
name: string;
age: number;
greet(): void;
update(data: any): Promise<void>;
}
type FnProps = FunctionPropertyNames<Example>;
// 'greet' | 'update'
// 提取特定类型的属性
type PropertyOfType<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
type StringProps = PropertyOfType<Example, string>;
// { name: string }
函数重载推断
// 获取重载函数的最后一个签名
type OverloadedReturnType<T> = T extends {
(...args: any[]): infer R;
(...args: any[]): infer R;
(...args: any[]): infer R;
(...args: any[]): infer R;
}
? R
: T extends (...args: any[]) => infer R
? R
: never;
递归条件类型
// 深度只读
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // 函数保持不变
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// 深度 Partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// 扁平化嵌套数组类型
type Flatten<T> = T extends Array<infer U>
? Flatten<U>
: T;
type Flat = Flatten<number[][][]>; // number
内置条件类型
// Exclude<T, U> - 从 T 中排除可赋值给 U 的类型
type Exclude<T, U> = T extends U ? never : T;
// Extract<T, U> - 从 T 中提取可赋值给 U 的类型
type Extract<T, U> = T extends U ? T : never;
// NonNullable<T> - 排除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// ReturnType<T> - 获取函数返回类型
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
// Parameters<T> - 获取函数参数类型
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// ConstructorParameters<T> - 获取构造函数参数类型
type ConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
// InstanceType<T> - 获取构造函数实例类型
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
常见面试问题
Q1: 实现 Exclude 和 Extract
答案:
// Exclude:从 T 中排除可赋值给 U 的类型
type MyExclude<T, U> = T extends U ? never : T;
// 工作原理(利用分发)
type Test = MyExclude<'a' | 'b' | 'c', 'a'>;
// = ('a' extends 'a' ? never : 'a')
// | ('b' extends 'a' ? never : 'b')
// | ('c' extends 'a' ? never : 'c')
// = never | 'b' | 'c'
// = 'b' | 'c'
// Extract:从 T 中提取可赋值给 U 的类型
type MyExtract<T, U> = T extends U ? T : never;
type Test2 = MyExtract<'a' | 'b' | 'c', 'a' | 'b'>;
// = 'a' | 'b'
Q2: 使用 infer 推断数组元素类型
答案:
// 基本实现
type ElementType<T> = T extends (infer E)[] ? E : never;
// 更完善:处理 readonly 数组
type ArrayElement<T> = T extends readonly (infer E)[] ? E : never;
// 测试
type E1 = ArrayElement<string[]>; // string
type E2 = ArrayElement<readonly number[]>; // number
type E3 = ArrayElement<[1, 2, 3]>; // 1 | 2 | 3
// 获取元组的第一个/最后一个元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type F = First<[1, 2, 3]>; // 1
type L = Last<[1, 2, 3]>; // 3
Q3: 实现 Awaited(解包 Promise)
答案:
// 递归解包 Promise
type MyAwaited<T> = T extends Promise<infer U>
? MyAwaited<U>
: T;
// 测试
type A1 = MyAwaited<Promise<string>>; // string
type A2 = MyAwaited<Promise<Promise<number>>>; // number
type A3 = MyAwaited<boolean>; // boolean
// 更完善:处理 PromiseLike
type Awaited2<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F): any }
? F extends (value: infer V) => any
? Awaited2<V>
: never
: T;
Q4: 条件类型中的分发是什么?如何阻止?
答案:
// 分发行为:联合类型会逐个应用条件类型
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// = ToArray<string> | ToArray<number>
// = string[] | number[]
// 不是 (string | number)[]
// 阻止分发:用元组包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>;
// = (string | number)[]
// 为什么?
// [string | number] extends [any] 作为整体判断
// 而不是分发到每个成员
// 实际应用:判断是否为 never
type IsNever<T> = [T] extends [never] ? true : false;
type Test1 = IsNever<never>; // true
type Test2 = IsNever<string>; // false
// 注意:不用 [T] 的话
type IsNeverBroken<T> = T extends never ? true : false;
type Test3 = IsNeverBroken<never>; // never!不是 true
// 因为 never 是空联合,分发后没有任何成员
Q5: 实现一个类型来提取对象中所有方法名
答案:
// 提取方法名
type MethodNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
// 更精确:区分属性函数和方法
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
// 获取方法类型
type Methods<T> = Pick<T, MethodNames<T>>;
// 测试
interface Example {
id: number;
name: string;
getName(): string;
setName(name: string): void;
callback: () => void; // 这也是函数
}
type Names = MethodNames<Example>;
// 'getName' | 'setName' | 'callback'
type Mths = Methods<Example>;
// { getName(): string; setName(name: string): void; callback: () => void }
// 只获取方法(不包括函数属性)
type PureMethodNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any
? K extends `${string}`
? K
: never
: never;
}[keyof T];