跳到主要内容

条件类型

问题

什么是 TypeScript 条件类型?如何使用 extendsinfer 进行类型判断和推断?

答案

条件类型允许根据类型关系选择不同的类型结果,语法类似三元表达式: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];

相关链接