Skip to content

条件类型

在大多数有用的程序的核心,我们必须根据输入来做决定。JavaScript 程序也不例外,但鉴于数值可以很容易地被内省,这些决定也是基于输入的类型。条件类型有助于描述输入和输出的类型之间的关系。

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

// type Example1 = number
type Example1 = Dog extends Animal ? number : string;

// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;

条件类型的形式看起来有点像 JavaScript 中的条件表达式( condition ? trueExpression : falseExpression )。

typescript
SomeType extends OtherType ? TrueType : FalseType;

当 extends 左边的类型可以赋值给右边的类型时,那么你将得到第一个分支中的类型("真 "分支); 否则你将得到后一个分支中的类型("假 "分支)。

从上面的例子来看,条件类型可能并不立即显得有用——我们可以告诉自己是否 Dog extends Animal ,并选择 number 或 string !但条件类型的威力来自于它所带来的好处。条件类型的力量来自于将它们与泛型一起使用。

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

typescript
interface IdLabel {
  id: number /* 一些字段 */;
}
interface NameLabel {
  name: string /* 另一些字段 */;
}

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 函数,该函数根据其输入的类型做出选择。注意一些事情:

  • 如果一个库必须在其 API 中反复做出同样的选择,这就会变得很麻烦。
  • 我们必须创建三个重载:一个用于确定类型的情况(一个用于 string ,一个用于 number ),一个用于最一般的情况(取一个 string | number )。对于 createLabel 所能处理的每一种新类型,重载的数量都会呈指数级增长。 相反,我们可以在一个条件类型中对该逻辑进行编码:
typescript
type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

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

typescript
interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}

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

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

// let a: NameLabel
let a = createLabel("typescript");

// let b: IdLabel
let b = createLabel(2.8);

// let c: NameLabel | IdLabel
let c = createLabel(Math.random() ? "hello" : 42);

条件类型约束

通常,条件类型中的检查会给我们提供一些新的信息。就像用类型守卫缩小范围可以给我们一个更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步约束泛型。

例如,让我们来看看下面的例子:

typescript
type MessageOf<T> = T["message"];

在这个例子中,TypeScript 出错是因为 T 不知道有一个叫做 message 的属性。我们可以对 T 进行约束,TypeScript 就不会再抱怨。

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

interface Email {
  message: string;
}

type EmailMessageContents = MessageOf<Email>;

然而,如果我们想让 MessageOf 接受任何类型,并在消息属性不可用的情况下,默认为 never 类型呢?我们可以通过将约束条件移出,并引入一个条件类型来做到这一点。

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

interface Email {
  message: string;
}

interface Dog {
  bark(): void;
}

// type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
const emc: EmailMessageContents = "balabala...";

// type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
const dmc: DogMessageContents = "error" as never;

在真正的分支中,TypeScript 知道 T 会有一个消息属性。

作为另一个例子,我们也可以写一个叫做 Flatten 的类型,将数组类型平铺到它们的元素类型上,但在其他方面则不做处理。

typescript
type Flatten<T> = T extends any[] ? T[number] : T;

// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;

// 单独一个类型。
// type Num = number
type Num = Flatten<number>;

当 Flatten 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取 string[] 的元素类型。 否则,它只是返回它被赋予的类型。

在条件类型内进行推理

我们只是发现自己使用条件类型来应用约束条件,然后提取出类型。这最终成为一种常见的操作,而条件类型使它变得更容易。

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

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

在这里,我们使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量,而不是指定如何在真实分支中检索 T 的元素类型。这使我们不必考虑如何挖掘和探测我们感兴趣的类型的结构。

我们可以使用 infer 关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数类型中提取出返回类型。

typescript
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

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

// type Str = string
type Str = GetReturnType<(x: string) => string>;

// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

// 给泛型传入 string 类型,条件类型会返回 never
type Never = GetReturnType<string>;
const nev: Never = "error" as never;

当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断(据推 测,这是最容许的万能情况)。不可能根据参数类型的列表来执行重载解析。

typescript
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;

// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;

分布式条件类型

当条件类型作用于一个通用类型时,当给定一个联合类型时,它们就变成了分布式的。例如,以下面的例子为例:

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

如果我们将一个联合类型插入 ToArray,那么条件类型将被应用于该联合的每个成员。

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

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

这里发生的情况是,StrArrOrNumArr 分布在:

typescript
string | number;

并对联合的每个成员类型进行映射,以达到有效的目的:

typescript
ToArray<string> | ToArray<number>;

这给我们留下了:

typeScript
string[] | number[];

通常情况下,分布性是需要的行为。为了避免这种行为,你可以用方括号包围 extends 关键字的每一边。

typeScript
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

// 'StrArrOrNumArr'不再是一个联合类型
// type StrArrOrNumArr = (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;