条件类型

在最有用的程序的核心,我们必须根据输入做出决定。JavaScript 程序也不例外,但考虑到值很容易内省,这些决定也基于输入的类型。条件类型有助于描述输入和输出类型之间的关系。

¥At the heart of most useful programs, we have to make decisions based on input. JavaScript programs are no different, but given the fact that values can be easily introspected, those decisions are also based on the types of the inputs. Conditional types help describe the relation between the types of inputs and outputs.

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):

¥Conditional types take a form that looks a little like conditional expressions (condition ? trueExpression : falseExpression) in JavaScript:

ts
SomeType extends OtherType ? TrueType : FalseType;
Try

extends 左边的类型可以赋值给右边的类型时,就会得到第一个分支(“true” 分支)的类型;否则,你将在后一个分支(“false” 分支)中获得类型。

¥When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).

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

¥From the examples above, conditional types might not immediately seem useful - we can tell ourselves whether or not Dog extends Animal and pick number or string! But the power of conditional types comes from using them with generics.

例如,让我们以下面的 createLabel 函数为例:

¥For example, let’s take the following createLabel function:

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 函数,该函数根据其输入的类型进行选择。注意几点:

¥These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things:

  1. 如果一个库必须在其 API 中一遍又一遍地做出相同的选择,这将变得很麻烦。

    ¥If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome.

  2. 我们必须创建三个重载:当我们确定类型时(一个用于 string,一个用于 number),一个用于每种情况,一个用于最一般的情况(采用 string | number)。对于 createLabel 可以处理的每个新类型,重载的数量呈指数增长。

    ¥We have to create three overloads: one for each case when we’re sure of the type (one for string and one for number), and one for the most general case (taking a string | number). For every new type createLabel can handle, the number of overloads grows exponentially.

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

¥Instead, we can encode that logic in a conditional type:

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

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

¥We can then use that conditional type to simplify our overloads down to a single function with no overloads.

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

条件类型约束

¥Conditional Type Constraints

通常,条件类型的检查会为我们提供一些新信息。就像使用类型保护进行缩小可以为我们提供更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步限制泛型。

¥Often, the checks in a conditional type will provide us with some new information. Just like narrowing with type guards can give us a more specific type, the true branch of a conditional type will further constrain generics by the type we check against.

例如,让我们采取以下措施:

¥For example, let’s take the following:

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 将不再抗诉:

¥In this example, TypeScript errors because T isn’t known to have a property called message. We could constrain T, and TypeScript would no longer complain:

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

但是,如果我们希望 MessageOf 采用任何类型,并且在 message 属性不可用时默认为类似 never 的东西怎么办?我们可以通过移出约束并引入条件类型来做到这一点:

¥However, what if we wanted MessageOf to take any type, and default to something like never if a message property isn’t available? We can do this by moving the constraint out and introducing a conditional type:

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

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

¥Within the true branch, TypeScript knows that T will have a message property.

再举一个例子,我们还可以编写一个名为 Flatten 的类型,将数组类型展平为它们的元素类型,但不处理它们:

¥As another example, we could also write a type called Flatten that flattens array types to their element types, but leaves them alone otherwise:

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[] 的元素类型。否则,它只返回给定的类型。

¥When Flatten is given an array type, it uses an indexed access with number to fetch out string[]’s element type. Otherwise, it just returns the type it was given.

在条件类型中推断

¥Inferring Within Conditional Types

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

¥We just found ourselves using conditional types to apply constraints and then extract out types. This ends up being such a common operation that conditional types make it easier.

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

¥Conditional types provide us with a way to infer from types we compare against in the true branch using the infer keyword. For example, we could have inferred the element type in Flatten instead of fetching it out “manually” with an indexed access type:

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

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

¥Here, we used the infer keyword to declaratively introduce a new generic type variable named Item instead of specifying how to retrieve the element type of Type within the true branch. This frees us from having to think about how to dig through and probing apart the structure of the types we’re interested in.

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

¥We can write some useful helper type aliases using the infer keyword. For example, for simple cases, we can extract the return type out from function types:

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

当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,会根据最后一个签名进行推断(这可能是最宽松的包罗万象的情况)。无法根据参数类型列表执行重载决议。

¥When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types.

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

分布式条件类型

¥Distributive Conditional Types

当条件类型作用于泛型类型时,它们在给定联合类型时变得可分配。例如,采取以下措施:

¥When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:

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

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

¥If we plug a union type into ToArray, then the conditional type will be applied to each member of that union.

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

这里发生的是 ToArray 分布在:

¥What happens here is that ToArray distributes on:

ts
string | number;
Try

并将联合的每个成员类型映射到有效的内容:

¥and maps over each member type of the union, to what is effectively:

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

这给我们留下了:

¥which leaves us with:

ts
string[] | number[];
Try

通常,分配性是期望的行为。为避免这种行为,你可以用方括号将 extends 关键字的每一侧括起来。

¥Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

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