深入感受TypeScript条件类型的强大魅力

215 阅读3分钟

引言

在 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的子类型,排除nullundefined, 包含原始类型和对象类型。

高级推导: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 类型系统中实现真正强大的类型逻辑和抽象能力,感受条件类型带来的魔力,大幅提升代码质量和开发体验。