条件类型

大多数有用程序的核心都需要根据输入做出决策。JavaScript 程序也不例外,但考虑到值可以轻松地进行内省,这些决策也基于输入的类型。条件类型有助于描述输入和输出类型之间的关系。

ts
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string;
type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;
type Example2 = string
Try

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

ts
SomeType extends OtherType ? TrueType : FalseType;
Try

extends 左侧的类型可分配给右侧的类型时,你将获得第一个分支(“true”分支)中的类型;否则,你将获得第二个分支(“false”分支)中的类型。

从上面的例子可以看出,条件类型可能并不立即显得有用 - 我们自己可以判断 Dog extends Animal 是否成立,并选择 numberstring!但条件类型的强大之处在于将其与泛型一起使用。

例如,让我们看一下以下 createLabel 函数

ts
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";
}
Try

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

  1. 如果库必须在其整个 API 中反复做出相同类型的选择,这就会变得很麻烦。
  2. 我们必须创建三个重载:一个用于我们确定类型的情况(一个用于 string,一个用于 number),以及一个用于最一般情况(接受 string | number)。对于 createLabel 可以处理的每种新类型,重载的数量呈指数级增长。

相反,我们可以将该逻辑编码到条件类型中

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

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

ts
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
 
let a = createLabel("typescript");
let a: NameLabel
 
let b = createLabel(2.8);
let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);
let c: NameLabel | IdLabel
Try

条件类型约束

通常,条件类型中的检查将为我们提供一些新信息。就像使用类型守卫进行缩小可以为我们提供更具体的类型一样,条件类型的 true 分支将通过我们检查的类型进一步约束泛型。

例如,让我们来看以下例子

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

在这个例子中,TypeScript 报错,因为 T 未知是否具有名为 message 的属性。我们可以约束 T,TypeScript 就不再报错

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

但是,如果我们希望 MessageOf 接受任何类型,并在 message 属性不可用时默认使用类似 never 的值呢?我们可以通过将约束移出并引入条件类型来实现

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

在真分支中,TypeScript 知道 T 具有 message 属性。

另一个例子,我们可以编写一个名为 Flatten 的类型,它将数组类型扁平化为其元素类型,但其他类型保持不变

ts
type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[]>;
type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>;
type Num = number
Try

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

在条件类型中推断

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

条件类型为我们提供了一种方法,可以使用 infer 关键字从我们在真分支中比较的类型中推断。例如,我们可以推断 Flatten 中的元素类型,而不是使用索引访问类型“手动”获取它

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

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

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

ts
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
 
type Num = GetReturnType<() => number>;
type Num = number
 
type Str = GetReturnType<(x: string) => string>;
type Str = string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
type Bools = boolean[]
Try

当从具有多个调用签名的类型(例如重载函数的类型)推断时,推断将从最后一个签名(可能是最宽松的 catch-all 案例)进行。无法根据参数类型列表执行重载解析。

ts
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>;
type T1 = string | number
Try

分布式条件类型

当条件类型作用于泛型类型时,在给定联合类型时,它们会变成分布式。例如,考虑以下

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

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

ts
type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>;
type StrArrOrNumArr = string[] | number[]
Try

这里发生的是 ToArray

ts
string | number;
Try

上进行分布,并映射联合的每个成员类型,实际上是

ts
ToArray<string> | ToArray<number>;
Try

这将给我们留下

ts
string[] | number[];
Try

通常,分布式是期望的行为。为了避免这种行为,可以在 extends 关键字的两侧用方括号括起来。

ts
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
type ArrOfStrOrNum = (string | number)[]
Try

TypeScript 文档是一个开源项目。帮助我们改进这些页面 通过发送拉取请求

此页面的贡献者
OTOrta Therox (10)
BKBenedikt König (1)
GFGeorge Flinn (1)
SFShinya Fujino (1)
NMNicolás Montone (1)
9+

上次更新时间:2024 年 3 月 21 日