条件类型的使用深度解析
深入解析TypeScript条件类型的extends关键字、分发特性与实际应用场景
条件类型的使用深度解析
一句话概括
条件类型是 TypeScript 类型系统的 if-else——它让类型能根据条件做判断,是构建高级工具类型和类型体操的基础。
背景
在前面的学习中,我们掌握了接口、泛型、枚举等基础能力。但有一些场景仅靠这些是解决不了的:
- “如何提取函数的返回值类型?”
- “如何判断两个类型是否相同?”
- “如何根据输入类型决定输出类型?”
这些都需要条件类型——在类型层面做条件判断的能力。
面试中,条件类型是 TypeScript 高级的分水岭:
- “什么是条件类型?” —— 进阶题
- “什么是分发条件类型?” —— 深度题
- “手写 ReturnType” —— 实战题
概念与定义
什么是条件类型?
条件类型的语法:
1
T extends U ? X : Y
含义:如果 T 可以赋值给 U(即 T 是 U 的子类型),则类型为 X,否则为 Y。
1
2
3
4
5
6
7
8
// 最简单的条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true("hello" 是 string 的子类型)
type D = IsString<any>; // boolean(any 的特殊行为)
type E = IsString<never>; // never(never 的特殊行为)
条件类型的本质
条件类型就是类型层面的三元表达式:
1
2
3
4
5
// 值层面
const result = typeof x === "string" ? "yes" : "no";
// 类型层面
type Result<T> = T extends string ? "yes" : "no";
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 判断一个类型是否是数组
type IsArray<T> = T extends any[] ? true : false;
type A1 = IsArray<string[]>; // true
type A2 = IsArray<number>; // false
type A3 = IsArray<readonly number[]>; // true(readonly number[] extends any[])
// 注意:严格来说 readonly number[] 不 extends number[]
// 需要特殊处理
// 更精确的数组判断
type IsArrayStrict<T> = T extends Array<unknown> ? true : false;
type A4 = IsArrayStrict<string[]>; // true
type A5 = IsArrayStrict<readonly string[]>; // false(readonly 数组不 extends Array)
核心知识点拆解
1. extends 关键字在条件类型中的含义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// extends 在条件类型中表示"类型兼容性"(赋值关系)
// T extends U 等价于:T 类型的值可以赋值给 U 类型的变量
// 示例
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
type IsAnimal<T> = T extends Animal ? "是动物" : "不是动物";
type R1 = IsAnimal<Dog>; // "是动物"(Dog 可以赋值给 Animal)
type R2 = IsAnimal<string>; // "不是动物"
// 字面量类型 extends 宽类型 → true
type R3 = IsAnimal<{ name: "旺财" }>; // "是动物"(包含 name 属性)
2. 分发条件类型(Distributive Conditional Types)
这是条件类型最关键也最容易踩坑的特性。
当条件类型作用于联合类型时,会自动分发:
1
2
3
4
5
6
7
type ToArray<T> = T extends any ? T[] : never;
// 当 T 是联合类型时,会分发
type Result = ToArray<string | number>;
// 等价于:ToArray<string> | ToArray<number>
// 即:string[] | number[]
// 不是:(string | number)[]
1
2
3
4
5
6
7
8
9
10
11
// 更直观的例子
type Wrap<T> = T extends any ? { value: T } : never;
type R = Wrap<string | number>;
// 等价于:Wrap<string> | Wrap<number>
// 即:{ value: string } | { value: number }
// 逐步分解过程:
// 1. Wrap<string | number>
// 2. 分发:Wrap<string> | Wrap<number>
// 3. 计算:{ value: string } | { value: number }
3. 阻止分发
有时候我们不希望分发,可以用元组包裹:
1
2
3
4
5
6
7
// 分发版本
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
// 阻止分发版本
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type B = ToArrayNoDistribute<string | number>; // (string | number)[]
1
2
3
4
5
6
7
8
9
10
11
12
// 实际应用:判断是否是联合类型
type IsUnion<T, U = T> = [T] extends [never]
? false
: T extends U
? [U] extends [T]
? false // 如果只有一个成员,U extends T 且 T extends U → 不是联合
: true // U extends T 但 T 不 extends U → 是联合
: false;
type U1 = IsUnion<string>; // false
type U2 = IsUnion<string | number>; // true
type U3 = IsUnion<never>; // false
4. 条件类型的嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类似 if-else if-else 的链式判断
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends Function ? "function" :
T extends any[] ? "array" :
T extends object ? "object" :
"unknown";
type T1 = TypeName<"hello">; // "string"
type T2 = TypeName<42>; // "number"
type T3 = TypeName<true>; // "boolean"
type T4 = TypeName<() => void>; // "function"
type T5 = TypeName<string[]>; // "array"
type T6 = TypeName<{ a: 1 }>; // "object"
type T7 = TypeName<null>; // "unknown"
5. 条件类型与 infer 关键字
infer 用于在条件类型中提取类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<(x: number) => boolean>; // boolean
type R3 = ReturnType<string>; // never(string 不是函数)
// 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type P1 = Parameters<(a: string, b: number) => void>; // [string, number]
type P2 = Parameters<(x?: boolean) => void>; // [boolean?]
// 提取 Promise 包裹的类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type U1 = UnwrapPromise<Promise<string>>; // string
type U2 = UnwrapPromise<number>; // number(非 Promise 原样返回)
// 递归解包嵌套 Promise
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U>
: T;
type D1 = DeepUnwrapPromise<Promise<Promise<Promise<number>>>>; // number
6. 条件类型与映射类型结合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 根据条件选择性添加属性
type FunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
type NonFunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
interface User {
name: string;
age: number;
greet(): string;
update(data: Partial<User>): void;
}
type UserMethods = FunctionProperties<User>;
// { greet: () => string; update: (data: Partial<User>) => void }
type UserData = NonFunctionProperties<User>;
// { name: string; age: number }
实战案例
案例一:类型安全的深拷贝返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 根据输入类型推断深拷贝后的返回类型
type DeepPartial<T> = T extends Function
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
database: {
host: string;
port: number;
};
cache: {
enabled: boolean;
ttl: number;
};
logging: {
level: "debug" | "info" | "error";
};
}
type PartialConfig = DeepPartial<Config>;
// {
// database?: { host?: string; port?: number };
// cache?: { enabled?: boolean; ttl?: number };
// logging?: { level?: "debug" | "info" | "error" };
// }
案例二:API 响应类型提取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 根据请求路径自动推断响应类型
interface ApiRoutes {
"/api/users": { users: { id: number; name: string }[]; total: number };
"/api/posts": { posts: { id: number; title: string }[] };
"/api/settings": { theme: string; language: string };
}
type ApiResponse<T extends keyof ApiRoutes> = ApiRoutes[T];
// 泛型请求函数
async function fetchApi<T extends keyof ApiRoutes>(
url: T
): Promise<ApiResponse<T>> {
const response = await fetch(url as string);
return response.json() as Promise<ApiResponse<T>>;
}
// 使用:返回类型自动推断
const users = await fetchApi("/api/users");
// users 类型: { users: { id: number; name: string }[]; total: number }
const settings = await fetchApi("/api/settings");
// settings 类型: { theme: string; language: string }
案例三:条件类型实现类型过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 从联合类型中过滤掉某些类型
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
// 使用
type AllTypes = string | number | boolean | null | undefined;
type NonNull = Exclude<AllTypes, null | undefined>;
// string | number | boolean
type OnlyString = Extract<AllTypes, string>;
// string
// 从对象类型中过滤属性
type PickByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface MixedObject {
name: string;
age: number;
active: boolean;
score: number;
email: string;
}
type StringProps = PickByType<MixedObject, string>;
// { name: string; email: string }
type NumberProps = PickByType<MixedObject, number>;
// { age: number; score: number }
底层原理
条件类型的编译时行为
条件类型是纯编译时的,不产生任何运行时代码:
1
2
3
4
5
6
7
// 源码
type IsString<T> = T extends string ? true : false;
const x: IsString<string> = true;
// 编译后
const x = true;
// 条件类型在编译阶段完成计算,运行时完全不存在
分发条件的实现机制
TypeScript 内部对联合类型的分发使用分配律:
1
2
3
4
5
6
7
8
9
10
11
// 内部处理
type Result = ToArray<A | B | C>;
// 编译器展开为:
type Result = ToArray<A> | ToArray<B> | ToArray<C>;
// 然后逐个计算:
// ToArray<A> → A[]
// ToArray<B> → B[]
// ToArray<C> → C[]
// 最终结果:A[] | B[] | C[]
infer 的推断算法
infer 的推断基于模式匹配:
1
2
3
4
5
6
7
8
9
10
11
12
13
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 对 T = (x: number) => string 的推断过程:
// 1. T 的结构: (x: number) => string
// 2. 模式: (...args: any[]) => infer R
// 3. 匹配成功,R 推断为 string
// 4. 返回 string
// 对 T = string 的推断过程:
// 1. T 的结构: string(基本类型)
// 2. 模式: (...args: any[]) => infer R
// 3. 匹配失败(string 不是函数类型)
// 4. 返回 never
💡 人话总结:
- 条件类型就是类型的 if-else:
T extends U ? X : Y - 分发条件类型:联合类型会被自动拆开,逐个判断再合并
infer就是在条件判断中”顺便提取”需要的类型- 阻止分发用
[T] extends [U]的元组包裹技巧
高频面试题解析
Q1:什么是分发条件类型?如何阻止分发?
参考答案: 当条件类型的检查参数是裸类型参数(naked type parameter)且传入联合类型时,TypeScript 会将联合类型拆开,对每个成员分别应用条件类型,最后取联合。
1
2
3
4
5
6
7
// 分发
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
// 阻止分发
type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNoDist<string | number>; // (string | number)[]
阻止分发的方法:用 [T] 替代 T,让类型参数不再是裸类型。
Q2:infer 关键字有什么用?举几个例子。
参考答案: infer 只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 提取函数返回值
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 2. 提取函数第一个参数
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // string
// 3. 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string
// 4. 提取 Promise 值类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<Promise<number>>>; // number
Q3:手写 Exclude 和 Extract。
参考答案:
1
2
3
4
5
6
7
8
9
10
// Exclude:从 T 中排除 U 的成员
type MyExclude<T, U> = T extends U ? never : T;
// Extract:从 T 中提取 U 的成员
type MyExtract<T, U> = T extends U ? T : never;
// 测试
type All = "a" | "b" | "c" | "d";
type WithoutAB = MyExclude<All, "a" | "b">; // "c" | "d"
type OnlyAB = MyExtract<All, "a" | "b">; // "a" | "b"
原理:分发条件类型。T extends U 对联合类型的每个成员分别判断。
Q4:如何判断一个类型是否是 never?
参考答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 直接判断不行,因为 never 是空联合类型,分发后什么都没有
type IsNever<T> = T extends never ? true : false;
type R = IsNever<never>; // never(不是 true!)
// 正确做法:阻止分发
type IsNever<T> = [T] extends [never] ? true : false;
type R = IsNever<never>; // true
type R2 = IsNever<string>; // false
// 原因:never 是空联合类型
// T extends never 会在分发时对 never 的每个成员判断
// 但 never 没有成员,所以结果是 never(空联合)
// [T] extends [never] 阻止了分发,将 never 包装在元组中判断
总结与扩展
核心要点
- 条件类型语法:
T extends U ? X : Y,类型层面的 if-else - 分发条件类型:裸类型参数 + 联合类型 = 自动分发
- 阻止分发:
[T] extends [U]元组包裹 - infer 关键字:在 extends 中提取类型变量
- 条件类型嵌套:实现链式类型判断
- 与映射类型结合:
as重映射实现属性过滤
延伸学习方向
- 映射类型:与条件类型配合的高级用法
- 类型守卫:运行时类型判断与条件类型的互补
- 模板字面量类型:字符串层面的条件类型
- type-challenges:GitHub 上的 TypeScript 类型体操题库
相关主题
本文由作者按照 CC BY 4.0 进行授权