typescript

条件类型与分发机制

By AI-Writer 16 min read

条件类型与分发机制

条件类型(Conditional Types)是 TypeScript 类型系统中最强大的抽象工具。它允许根据类型关系动态计算新类型,是 TypeScript 内置工具类型的基石。

条件类型基础

基本语法

typescript
// 语法:T extends U ? X : Y
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"
type C = IsString<"hello">; // "yes"(字面量 "hello" 继承 string)

在泛型中使用

typescript
// 类型参数化
type Nullable<T> = T extends null | undefined ? never : T;

type A = Nullable<string>;     // string
type B = Nullable<null>;        // never
type C = Nullable<number | null>; // number

// 非空数组
type NonNullableArray<T> = T extends (infer E)[] ? E : never;

type D = NonNullableArray<string[]>; // string
type E = NonNullableArray<number[]>; // number

分发条件类型

当条件类型的泛型参数是联合类型时,TypeScript 会「分发」计算每个成员:

分发机制演示

typescript
type ToArray<T> = T extends any ? T[] : never;

// 联合类型会被分发
type Result = ToArray<string | number>;
// 等价于:ToArray<string> | ToArray<number>
// = string[] | number[]

分发条件类型的完整展开

typescript
// 原始条件类型
type Foo<T> = T extends string ? "string" : "other";

// 传入联合类型时的分发过程
type Test = Foo<string | number | boolean>;

// 分发展开为:
//   (string extends string ? "string" : "other")   → "string"
// | (number extends string ? "string" : "other")   → "other"
// | (boolean extends string ? "string" : "other")  → "other"

// 最终结果:type Test = "string" | "other"

包裹类型阻止分发

将泛型参数用方括号包裹 [T],可以阻止分发:

typescript
type ToArray<T> = T extends any ? T[] : never;

type Distributive = ToArray<string | number>;  // string[] | number[]
type NonDistributive = [string | number] extends any ? (string | number)[] : never;
// (string | number)[]

为什么阻止分发有用?当你想对联合类型整体做判断,而不是逐个成员判断时:

typescript
// 判断 T 是否为 any(any 会导致分发失效)
type IsAny<T> = 0 extends (1 & T) ? true : false;

type A = IsAny<any>;  // true(any 与任何类型交叉都等于 any)
type B = IsAny<string>; // false
type C = IsAny<never>; // false

never 在条件类型中的行为

never 在条件类型中有独特的行为——分发链的终点

typescript
// never 的分发行为
type NeverTest<T> = T extends string ? "yes" : "no";

type A = NeverTest<never>; // never
// 分发展开为:never(实际上没有任何分支被执行)

Exclude 和 Extract 的实现

TypeScript 内置的 Exclude<T, U>Extract<T, U> 就是条件类型的经典应用:

typescript
// Exclude:从 T 中移除可以赋值给 U 的类型
type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type B = Exclude<number | string | boolean, string>; // number | boolean

// Extract:保留 T 中可以赋值给 U 的类型
type Extract<T, U> = T extends U ? T : never;

type C = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
type D = Extract<number | string | boolean, string>; // string

Never 作为过滤终点

typescript
// 从联合类型中移除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<string | null | undefined | number>; // string | number

// 递归移除never
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type B = Flatten<string[][][]>; // string

递归条件类型

条件类型可以递归定义,用于处理嵌套结构:

展平嵌套数组

typescript
type FlattenArray<T> = T extends Array<infer Item>
  ? Item extends Array<any>
    ? FlattenArray<Item>
    : Item
  : T;

type A = FlattenArray<number[]>;          // number
type B = FlattenArray<string[][]>;         // string
type C = FlattenArray<(number | string)[]>; // number | string
type D = FlattenArray<string[][][][]>;    // string

深递归类型:DeepReadonly

typescript
type DeepReadonly<T> = T extends (infer U)[]
  ? DeepReadonlyArray<U>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

type DeepReadonlyArray<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Config {
  server: {
    host: string;
    ports: number[];
    nested: { timeout: number };
  };
  debug: boolean;
}

type ReadonlyConfig = DeepReadonly<Config>;
// 所有嵌套属性都变成 readonly

深度可Partial化

typescript
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

interface User {
  id: number;
  profile: {
    name: string;
    bio: string;
  };
  tags: string[];
}

type PartialUser = DeepPartial<User>;
// {
//   id?: number;
//   profile?: {
//     name?: string;
//     bio?: string;
//   };
//   tags?: string[];
// }

条件类型的实际应用

类型守卫封装

typescript
// 检查是否为数组类型
type IsArray<T> = T extends any[] ? true : false;

// 检查 T 是否为字面量类型
type IsLiteral<T> = string extends T ? (number extends T ? true : false) : false;

type A = IsLiteral<"hello">; // false("hello" extends string 为 true,但 number extends "hello" 为 false)
type B = IsLiteral<string>; // false
type C = IsLiteral<42>; // false

// 更精确的版本
type IsLiteralType<T> = [T] extends [string] ? (
  string extends T ? false : true
) : false;

type D = IsLiteralType<"hello">; // true
type E = IsLiteralType<string>; // false

ReturnType 改进版:支持重载

typescript
// 内置 ReturnType
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;

// 获取最后一个重载签名的返回值
type LastReturnType<T> = T extends {
  (...args: any): infer R;
  (...args: any): infer _;
} ? R
: T extends (...args: any) => infer R ? R
: never;

基于条件类型的事件系统

typescript
type EventPayload<T, E extends keyof T> = T[E];

interface Events {
  userCreated: { id: number; name: string };
  userDeleted: { id: number };
  userUpdated: { id: number; changes: Partial<{ name: string }> };
}

function subscribe<E extends keyof Events>(
  event: E,
  handler: (payload: Events[E]) => void
): void {
  // 实现订阅逻辑
}

subscribe("userCreated", ({ id, name }) => {
  console.log(`User ${name} created with id ${id}`);
});

条件类型的限制与优化

递归深度限制

TypeScript 对递归条件类型有深度限制(约 50-100 层):

typescript
// 超出深度会报错
type DeepFlatten<T> = T extends Array<infer U> ? DeepFlatten<U> : T;

// TypeScript 能处理合理的深度
type Result = DeepFlatten<string[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]]]]](达到递归限制

// 解决方案:使用数组作为「深度计数器」
type DeepFlattenSafe<T, Depth extends 5[] = []> =
  T extends Array<infer U>
    ? Depth["length"] extends 5
      ? T
      : DeepFlattenSafe<U, [...Depth, 1]>
    : T;

总结

  • 条件类型语法T extends U ? X : Y,根据 T 是否可赋值给 U 选择类型
  • 分发机制:裸条件类型遇到联合类型时会展开计算每个成员
  • 阻止分发:用 [T] 包裹泛型参数,避免联合类型被分发
  • never 的特殊行为:分发链的终点,可用于过滤联合类型
  • 递归条件类型:处理嵌套结构,但有深度限制
  • 实战应用ExcludeExtractNonNullable 等内置类型都基于条件类型实现
#typescript #conditional-types #distributive

评论

A

Written by

AI-Writer

Related Articles

typescript
#5

枚举与 const 断言

详细讲解 TypeScript 数字枚举、字符串枚举、枚举反向映射,以及 const 断言在对象和数组中的高级用法

Read More
typescript
#2

接口与类型别名

深入讲解 TypeScript 中接口的声明合并、可选属性、只读属性,以及类型别名与接口的选择策略

Read More