阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 条件类型与分布式条件类型

条件类型与分布式条件类型

作者:陈川 阅读数:55545人阅读 分类: TypeScript

TypeScript 的条件类型和分布式条件类型是类型系统中的高级特性,能够基于输入类型动态推导出更复杂的类型关系。它们通过类型层面的条件分支和自动分发机制,显著增强了类型编程的灵活性。

条件类型基础

条件类型的语法类似于三元表达式,形式为 T extends U ? X : Y。当类型 T 可赋值给类型 U 时,返回类型 X,否则返回类型 Y。这种机制允许在类型层面实现条件判断:

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<42>;      // false

条件类型常与泛型结合使用,创建动态的类型推导。例如实现一个提取数组元素类型的工具类型:

type ElementType<T> = T extends (infer U)[] ? U : never;

type Numbers = ElementType<number[]>;   // number
type Mixed = ElementType<(string | boolean)[]>; // string | boolean

分布式条件类型

当条件类型作用于联合类型时,会发生分布式行为。这是 TypeScript 的类型系统中最强大的特性之一。具体表现为:对于 T extends U ? X : Y,如果 T 是联合类型 A | B | C,则结果会分发为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

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

type Numbers = ToArray<number>;         // number[]
type UnionArray = ToArray<number | string>; // number[] | string[]

分布式特性在过滤联合类型时特别有用。例如实现一个从联合类型中排除特定类型的工具:

type Exclude<T, U> = T extends U ? never : T;

type WithoutNumbers = Exclude<string | number | boolean, number>; // string | boolean

条件类型中的类型推断

infer 关键字允许在条件类型中声明临时类型变量,用于捕获深层类型信息。这在解构复杂类型时非常有用:

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type StringResult = UnwrapPromise<Promise<string>>; // string
type NumberResult = UnwrapPromise<number>;          // number

多层嵌套的 infer 可以处理更复杂的场景。例如提取函数返回的 Promise 的解析类型:

type UnwrapPromiseReturn<T> = 
    T extends (...args: any[]) => Promise<infer U> ? U :
    T extends (...args: any[]) => infer V ? V :
    never;

type A = UnwrapPromiseReturn<() => Promise<number>>; // number
type B = UnwrapPromiseReturn<() => string>;         // string

禁用分布式行为

有时需要禁用分布式行为,可以通过将检查的类型包装在元组中实现:

type NonDistributive<T> = [T] extends [any] ? true : false;

// 非分布式行为
type Test1 = NonDistributive<string | number>; // true

// 对比分布式行为
type Distributive<T> = T extends any ? true : false;
type Test2 = Distributive<string | number>;    // true | true

实际应用示例

条件类型在 React 属性类型处理中很实用。例如创建一个根据组件 props 动态决定 children 类型的工具:

type PropsWithChildren<P> = 
    'children' extends keyof P 
    ? P 
    : P & { children?: React.ReactNode };

function createComponent<P>(props: PropsWithChildren<P>) {
    // 组件实现
}

// 自动推断 children 类型
createComponent({ value: 1 });                  // children 可选
createComponent({ children: <div/>, value: 1 }); // children 已存在

另一个典型应用是处理 Redux action 类型:

type Action<T extends string, P = void> = {
    type: T;
    payload: P;
};

type ExtractAction<A, T> = A extends Action<infer U, infer P> 
    ? U extends T 
        ? P 
        : never 
    : never;

type MyActions = 
    | Action<'ADD', number> 
    | Action<'REMOVE', string>;

type AddPayload = ExtractAction<MyActions, 'ADD'>;   // number
type RemovePayload = ExtractAction<MyActions, 'REMOVE'>; // string

递归条件类型

TypeScript 4.1 引入了递归条件类型,允许类型自引用。这使得处理嵌套结构成为可能:

type DeepReadonly<T> = 
    T extends object 
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> } 
    : T;

type NestedData = {
    a: number;
    b: {
        c: string;
        d: boolean[];
    };
};

type ReadonlyData = DeepReadonly<NestedData>;
/* 等价于:
{
    readonly a: number;
    readonly b: {
        readonly c: string;
        readonly d: readonly boolean[];
    };
}
*/

递归条件类型还能实现类型层面的数组操作,比如反转元组:

type Reverse<T extends any[]> = 
    T extends [infer First, ...infer Rest] 
    ? [...Reverse<Rest>, First] 
    : [];

type Numbers = [1, 2, 3, 4];
type Reversed = Reverse<Numbers>; // [4, 3, 2, 1]

条件类型与模板字面量类型

结合 TypeScript 4.1 的模板字面量类型,条件类型可以实现更强大的字符串操作:

type ExtractRouteParams<T> = 
    T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type Params = ExtractRouteParams<'/user/:id/posts/:postId'>;
/* 类型为:
{
    id: string;
    postId: string;
}
*/

性能考量

复杂条件类型可能导致类型检查变慢,特别是在递归深度较大时。可以通过以下方式优化:

  1. 避免过深的递归层级
  2. 使用尾递归优化(TypeScript 4.5+)
  3. 对大型联合类型提前进行过滤
// 尾递归优化示例
type TrimLeft<T extends string> = 
    T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;

type Trimmed = TrimLeft<'   hello'>; // "hello"

条件类型与映射类型结合

结合映射类型可以创建更灵活的类型转换工具。例如实现一个将接口所有方法返回类型包装为 Promise 的工具:

type Promisify<T> = {
    [K in keyof T]: T[K] extends (...args: infer A) => infer R
        ? (...args: A) => Promise<R>
        : T[K];
};

interface Api {
    getUser(id: number): { name: string };
    checkStatus(): boolean;
}

type AsyncApi = Promisify<Api>;
/* 等价于:
{
    getUser(id: number): Promise<{ name: string }>;
    checkStatus(): Promise<boolean>;
}
*/

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌