TypeScript类型操作之条件类型

461 阅读3分钟

我在之前一篇关于泛型的文章中提到,当我们需要写一个返回值类型与入参类型保持一致(即传入什么类型就返回什么类型)的函数时可以使用泛型,如下:

function identity<Type>(arg: Type): Type {
  return arg;
}

但是假如我们需要的不是保持一致;而是如果传入string类型那么返回类型就是{ name: string; },如果传入number类型那么返回值就是{ id: number },这种情况应该如何写呢?下面我们将来解决这个问题。

TypeScipt中的三元表达式

在js中我们经常会看到以下代码,以下代码描述了当a大于1时isLargeThanOne的值就是true,否则isLargeThanOne的值为false

const isLargeThanOne = a > 1 ? true : false

而在ts条件类型中也有相似的语法,请看下面代码:

interface IdLabel {
    id: number;
}
interface NameLabel {
    name: string;
}

type NameOrId<T extends number | string> = T extends number
    ? IdLabel
    : NameLabel;
    
function createLabel<T extends string | number>(idOrName: T): NameOrId<T> {
    // ...
}

let a = createLabel("typescript"); // NameLabel
let b = createLabel(2); // IdLabel

ts中的条件类型与js中的三元表达式十分相似,唯一需要理解一下的是extends关键字。当extends左边的类型可以赋值给右边类型时,你得到的类型就是第一个分支(true分支)的类型,否则你得到的就是第二个分支(false分支)的类型。

条件类型约束

如下代码,当我们尝试对一个未知类型读取message时ts会抛出警告;因为ts编译器并不认为T是具有message属性的。

type MessageOf<T> = T['message']; // Type '"message"' cannot be used to index type 'T'.

我们可以通过extends关键字来约束传入的T必须带有message属性。

type MessageOf<T extends { message: unknown }> = T['message'];

interface Email {
    message: string;
}

type EmailMessageContents = MessageOf<Email>; // string
type n = MessageOf<number>; // -   Type 'number' does not satisfy the constraint '{ message: unknown; }'.

然而当我们想要T可以是任意类型,当T不含有message属性时返回never应该如何实现呢?请看下面代码:

type MessageOf<T> = T extends { message: unknown } ? T['message'] : never;

type EmailMessageContents = MessageOf<Email>; // string
type n = MessageOf<number>; // never

infer

如下实现了一个flatten函数,当传入是数组时返回数组的第一个元素,否则返回入参本身。

function flatten(arg) {
    return Array.isArray(arg) ? arg[0] : arg
}

那么在ts中如何描述这个函数呢?请看下面代码

type Flatten<T> = (arg: T) => T extends any[] ? T[number] : T

这样实现似乎很完美,但是当我们想要在很深的结构中取出我们想要的类型就会比较麻烦,可能是这样的T[number]['a'][number]['b'][number]['c']...

ts提供了infer关键字可以解决这个问题,请看下面代码:

type Flatten<T> = (arg: T) => T extends Array<infer Item> ? Item : T

infer关键字使我们可以声明式的引入一个新的泛型变量,来使得我们可以不必考虑如何分离和挖掘我们感兴趣的类型的结构。在这个例子中本质上就相当于type Item = T[number]

infer关键字可以帮助我们写出很多工具类型,如果下面的例子,提取出函数的返回值:

type ReturnType<T> = T extends (...args: never[]) => infer Return ? Return : never

type Num = ReturnType<() => number>; // number

总结

js相较于其他如c/java等语言来说性能是确实不如的,但是js强大的灵活性使得我们可以秀出很多“骚操作”。ts虽然引入了静态类型检查,但是却没有影响到灵活性。其很大的原因要归功于泛型和条件类型的存在。