引言
在 TypeScript 的类型系统中,条件类型(Conditional Types)是类型层面的条件判断机制,允许我们根据输入类型动态决定输出类型。 它们为类型系统带来了类似编程语言中的 if/else 逻辑,我们可以像写函数逻辑那样写条件类型,极大地增强了类型的灵活性和表达能力。
举一个简单的例子,判断一个类型是否为 true,如果是,则输出类型为 T,否则输出类型为 F。
type If<C extends boolean, T, F> = C extends true ? T : F;
type A = If<true, 'a', 'b'>; // 'a'
条件类型基础
- 条件类型由三元表达式(三元运算符)和 extends 关键字组成,通过真假分支返回结果。
- 条件类型可以表示为
A extends B ? X : Y,其中 A 和 B 为类型表达式,X 和 Y 为类型表达式。 - 条件类型可以嵌套使用,根据自己的需求去进行复杂的逻辑判断,例如:
A extends B ? X : Y extends Z ? X : Y。 举一个例子:
// 判断类型是否为 number
type IsNumber<T> = T extends number ? true : false;
type Res = IsNumber<123>; // true
分布式条件类型:联合类型处理
当条件类型作用于裸类型参数(泛型参数是否裸露)时,TypeScript 会对联合类型进行分布式计算:
当条件类型用于泛型参数,并且传入的是联合类型时,TypeScript 会自动把联合类型的每个成员单独带入条件类型中计算,最后把结果合并成新的联合类型。 简单来说就是,分布式计算让条件类型对联合类型“自动拆开”分别判断,最后再合并结果。
举一个例子:
/**
* (123 | true) extends boolean
* => 123 extends boolean ? "Y" : "N" | true extends boolean ? "Y" : "N"
* => "N" | "Y"
*/
type Distributed<T> = T extends boolean ? "Y" : "N";
type Res = Distributed<123 | true>; // "Y" | "N"
阻止分发:控制类型计算
上面介绍了类型类型处理的时候会进行分布式条件处理,往往这种效果并不是我们想要的,我们是需要整体的计算而不是去拆分计算。
像上面的那个例子中,我们就想要发挥的类型是N,而不是"Y" | "N",因为是一个整体的计算,而不是单个拆分出来计算。
在这里目前有三种方法可以阻止分发这个问题:
- 元组包装法
type NoDistribute<T> = [T] extends [boolean] ? "Y" : "N";
type Res = NoDistribute<"123" | true>; // "N"
- 函数包装法
type NoDistribute<T> = ((arg: T) => void) extends ((arg: boolean) => void ) ? "Y" : "N";
type Res = NoDistribute<"123" | true>; // "N"
/**
ps:
其实不太推荐这种方法,
type X = NoDistribute<"123" | true | false>; // "Y"
这种情况就不能彻底阻止分发
因为 TypeScript 的函数参数双向协变机制导致判断结果和元组/对象包装不同。
*/
- 交叉类型{}法
type NoDistribute<T> = T & {} extends boolean ? "Y" : "N";
type Res = NoDistribute<"123" | true>; // "N"
值得一提的是,在 TypeScript 中,{}的作用是创建一个空对象,而{} & T的作用是创建一个T的子类型,排除
null和undefined, 包含原始类型和对象类型。
高级推导:infer
TypeScript 中支持通过 infer 关键字来在条件类型中提取类型的某一部分信息, 并且我们可以通过infer 关键字可以让我们提取类型中的某一部分信息,然后使用这个信息来构造新的类型。
比如官方的 ReturnType 就是获取函数返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type Func = (a: string, b: number) => boolean;
type Result = ReturnType<Func>; // boolean
再举几个例子
// 提取首尾两个
type StartAndEnd<T extends unknown[]> = T extends [
infer Start,
...unknown[],
infer End
]
? [Start, End]
: T;
type TestArray = [1, 2, 3, 4, 5];
type Result = StartAndEnd<TestArray>; // [1, 5]
// 递归提取 - 递归提取 T 的类型,直到找到非 Promise 的类型为止
type PromiseValue<T> = T extends Promise<infer U> ? PromiseValue<U> : T;
type NestedPromise = PromiseValue<Promise<Promise<string>>>; // string
总结
条件类型看着并不难,但我们需要在实践过程中不断运用,熟能生巧,渐而渐之,便会掌握条件类型,将使你能够在 TypeScript 类型系统中实现真正强大的类型逻辑和抽象能力,感受条件类型带来的魔力,大幅提升代码质量和开发体验。