条件类型
在大多数有用的程序核心中,我们必须根据输入做出决策。 JavaScript 程序也不例外,但鉴于值可以很容易地被自省,这些决策也基于输入的类型。 条件类型帮助描述输入和输出类型之间的关系。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
type Example2 = RegExp extends Animal ? number : string;
Try条件类型的形式看起来有点像 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression):
当 extends 左边的类型可以赋值给右边的类型时,你将获得第一个分支(“true”分支)中的类型;否则你将获得后一个分支(“false”分支)中的类型。
从上面的例子来看,条件类型可能看起来并不是立即有用的——我们可以自己判断 Dog extends Animal 是否成立,然后选择 number 或 string! 但条件类型的强大之处在于将它们与泛型一起使用。
例如,让我们来看下面的 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";
}Try这些 createLabel 的重载描述了一个单一的 JavaScript 函数,该函数根据其输入的类型做出选择。注意几点:
- 如果一个库必须在其 API 中反复做出相同类型的选择,这会变得很繁琐。
- 我们必须创建三个重载:一个用于当我们确定类型的情况(一个用于
string,一个用于number),还有一个用于最一般的情况(接受string | number)。对于createLabel可以处理的每一种新类型,重载的数量呈指数级增长。
相反,我们可以将该逻辑编码在一个条件类型中:
然后我们可以使用该条件类型将我们的重载简化为一个没有重载的函数。
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
let b = createLabel(2.8);
let c = createLabel(Math.random() ? "hello" : 42);
Try条件类型约束
通常,条件类型中的检查会为我们提供一些新信息。 就像使用类型守卫进行 narrowing 可以给我们一个更具体的类型一样,条件类型的 true 分支将通过我们检查的类型进一步约束泛型。
例如,让我们看下面的代码:
在这个例子中,TypeScript 报错,因为不知道 T 有一个名为 message 的属性。 我们可以约束 T,这样 TypeScript 就不再报错:
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
Try但是,如果我们希望 MessageOf 接受任何类型,并且如果没有 message 属性则默认为类似 never 的类型,该怎么办? 我们可以通过将约束移出并引入条件类型来实现:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
type DogMessageContents = MessageOf<Dog>;
Try在 true 分支中,TypeScript 知道 T 将会有一个 message 属性。
作为另一个例子,我们还可以编写一个名为 Flatten 的类型,它将数组类型展平为其元素类型,否则保持不变:
type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
type Str = Flatten<string[]>;
// 保持原类型不变。
type Num = Flatten<number>;
Try当 Flatten 被赋予一个数组类型时,它使用带有 number 的索引访问来获取 string[] 的元素类型。 否则,它只是返回给定的类型。
在条件类型中推断
我们刚刚发现自己使用条件类型来应用约束,然后提取类型。 这最终成为一种常见的操作,以至于条件类型使其变得更加容易。
条件类型为我们提供了一种使用 infer 关键字从我们在 true 分支中比较的类型中进行推断的方法。 例如,我们可以在 Flatten 中推断元素类型,而不是使用索引访问类型“手动”获取它:
在这里,我们使用 infer 关键字以声明方式引入了一个新的泛型类型变量 Item,而不是指定如何在 true 分支中检索 Type 的元素类型。 这使我们不必考虑如何深入挖掘和探测我们感兴趣类型的结构。
我们可以使用 infer 关键字编写一些有用的辅助类型别名。 例如,对于简单的情况,我们可以从函数类型中提取返回类型:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>;
type Str = GetReturnType<(x: string) => string>;
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
Try当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,推断是从最后一个签名进行的(该签名大概是包罗万象的最宽松的情况)。不可能基于参数类型列表执行重载解析。
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>;
Try分布式条件类型
当条件类型作用于泛型类型时,当给定一个联合类型时,它们会变成分布式的。 例如,看下面的代码:
如果我们将一个联合类型插入 ToArray,那么条件类型将应用于该联合的每个成员。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
Try这里发生的事情是 ToArray 分布在:
并映射联合的每个成员类型,实际上等同于:
这给我们留下了:
通常,分布性是期望的行为。 要避免这种行为,你可以用方括号括起 extends 关键字的每一侧。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'ArrOfStrOrNum' 不再是联合类型。
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
Try