TypeScript 条件类型

Flying
2021-10-30 / 0 评论 / 122 阅读 / 正在检测是否收录...

TypeScript 的类型系统允许根据先前的类型进行逻辑检查来创建新的构造(类型)。它通过条件类型的概念来实现,条件类型根据现有类型的情况解析为两种可能的类型之一。在大多数有用的程序中,我们需要根据输入做出决策。JavaScript程序也不例外,但考虑到可以轻松地检查值,这些决策也基于输入的类型。条件类型有助于描述输入和输出类型之间的关系。

condition-ts.svg

语法

条件类型的形式有点类似于JavaScript中的条件表达式(condition ? trueExpression : falseExpression):

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

type NumderType = Dog extends Animal ? number : string;
type StringType = RegExp extends Animal ? number : string;

extends 关键字左侧的类型可以赋值给右侧的类型时,将得到第一个分支("true"分支)中的类型;否则将得到后一个分支("false"分支)中的类型。

从上面的例子中可以看出,条件类型可能一开始看起来并不那么有用——我们可以自己判断 Dog extends Animal 并选择 numberstring!但是条件类型的威力来自于与泛型一起使用。

泛型条件类型

例如,让我们来看下面的createLabel函数:

interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

这些 createLabel 的重载描述了一个基于输入类型做出选择的单个 JavaScript 函数。请注意以下几点:

  1. 如果一个库在其 API 中需要反复进行相同类型的选择,这将变得繁琐。
  2. 我们需要创建三个重载:一个用于确定类型时的情况(一个用于 string 和一个用于 number),以及一个用于最一般情况的重载(接受 string | number)。对于每一种 createLabel 可以处理的新类型,重载的数量呈指数级增长。

相反,我们可以使用条件类型将该逻辑编码进去:

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

我们可以使用该条件类型将我们的重载简化为一个没有重载的单个函数。

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}
 
let nameLabel = createLabel("typescript");
let idLabel = createLabel(2.8);  
let idOrName = createLabel(Math.random() ? "hello" : 42);

条件类型约束

通常,条件类型中的检查会为我们提供一些新信息。就像通过类型保护进行缩小可以给我们一个更具体的类型一样,条件类型的 true 分支将根据我们检查的类型进一步约束泛型。

例如,让我们看下面的示例:

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

在这个示例中,TypeScript 报错,因为 T 没有具有名为 message 的属性。我们可以约束 T,这样 TypeScript 就不会再抱怨了:

type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>;

然而,如果我们希望 MessageOf 接受任意类型,并在没有 message 属性时默认为 never,该怎么办呢?我们可以将约束移到外面,并引入一个条件类型:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
// EmailMessageContents: string
type EmailMessageContents = MessageOf<Email>;
// DogMessageContents: never
type DogMessageContents = MessageOf<Dog>;

在 true 分支中,TypeScript 知道 T 将具有一个 message 属性。

作为另一个例子,我们还可以编写一个名为 Flatten 的类型,将数组类型展平为元素类型,否则保持不变:

type Flatten<T> = T extends any[] ? T[number] : T;
 
// 提取出元素类型。
type Str = Flatten<string[]>;
 
// 保持不变。
type Num = Flatten<number>;

当 Flatten 接收一个数组类型时,它使用带有 number 索引访问来提取出 string[] 的元素类型。否则,它只是返回它所接收到的类型。

在条件类型中进行类型推断

我们刚刚发现使用条件类型来应用约束条件,然后提取出类型。这实际上是一种很常见的操作,条件类型使得这个过程更加容易。

条件类型提供了一种方式,我们可以使用 infer 关键字在 true 分支中对比较的类型进行推断。例如,我们可以在 Flatten 中推断出元素类型,而不是通过索引访问类型手动提取出来:

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

在这里,我们使用 infer 关键字声明性地引入一个名为 Item 的新的泛型类型变量,而不是在 true 分支中指定如何获取 T 的元素类型。这使我们不必考虑如何深入和分解我们感兴趣的类型的结构。

分发式条件类型

当条件类型作用于泛型类型时,如果给定一个联合类型,它们将变成分发式条件类型。例如,看下面的例子:

type StrArrOrNumArr<T> = T extends string ? string[] : number[];

type Result = StrArrOrNumArr<string | number>;

如果我们将一个联合类型传递给 StrArrOrNumArr,那么条件类型将应用于联合类型的每个成员。

这里发生的情况是,StrArrOrNumArr 在以下类型上进行分发:

(string | number)

并且对联合类型的每个成员类型进行映射,实际上变成了:

(string extends string ? string[] : number[]) | (number extends string ? string[] : number[])

这将得到:

string[] | number[]

通常,分发性是期望的行为。如果要避免这种行为,可以在 extends 关键字的两边加上方括号。

type StrArrOrNumArr<T> = [T] extends [string] ? string[] : number[];

type Result = StrArrOrNumArr<string | number>;

参考

TypeScript 文档

1

评论 (0)

取消