文章

条件类型的使用深度解析

深入解析TypeScript条件类型的extends关键字、分发特性与实际应用场景

条件类型的使用深度解析

一句话概括

条件类型是 TypeScript 类型系统的 if-else——它让类型能根据条件做判断,是构建高级工具类型和类型体操的基础。

背景

在前面的学习中,我们掌握了接口、泛型、枚举等基础能力。但有一些场景仅靠这些是解决不了的:

  • “如何提取函数的返回值类型?”
  • “如何判断两个类型是否相同?”
  • “如何根据输入类型决定输出类型?”

这些都需要条件类型——在类型层面做条件判断的能力。

面试中,条件类型是 TypeScript 高级的分水岭:

  • “什么是条件类型?” —— 进阶题
  • “什么是分发条件类型?” —— 深度题
  • “手写 ReturnType” —— 实战题

概念与定义

什么是条件类型?

条件类型的语法:

1
T extends U ? X : Y

含义:如果 T 可以赋值给 U(即 TU 的子类型),则类型为 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 包装在元组中判断

总结与扩展

核心要点

  1. 条件类型语法T extends U ? X : Y,类型层面的 if-else
  2. 分发条件类型:裸类型参数 + 联合类型 = 自动分发
  3. 阻止分发[T] extends [U] 元组包裹
  4. infer 关键字:在 extends 中提取类型变量
  5. 条件类型嵌套:实现链式类型判断
  6. 与映射类型结合as 重映射实现属性过滤

延伸学习方向

  • 映射类型:与条件类型配合的高级用法
  • 类型守卫:运行时类型判断与条件类型的互补
  • 模板字面量类型:字符串层面的条件类型
  • type-challenges:GitHub 上的 TypeScript 类型体操题库

相关主题

本文由作者按照 CC BY 4.0 进行授权