乐趣区

关于javascript:TypeScript-之-Conditional-Types

前言

TypeScript 的官网文档早已更新,但我能找到的中文文档都还停留在比拟老的版本。所以对其中新增以及订正较多的一些章节进行了翻译整顿。

本篇整顿自 TypeScript Handbook 中「Conditional Types」章节。

本文并不严格依照原文翻译,对局部内容也做了解释补充。

条件类型(Conditional Types)

很多时候,咱们须要基于输出的值来决定输入的值,同样咱们也须要基于输出的值的类型来决定输入的值的类型。 条件类型(Conditional types)就是用来帮忙咱们形容输出类型和输入类型之间的关系。

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

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

SomeType extends OtherType ? TrueType : FalseType;

单从这个例子,可能看不出条件类型有什么用,但当搭配泛型应用的时候就很有用了,让咱们拿上面的 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 是如何基于输出值的类型不同而做出不同的决策,返回不同的类型。留神这样一些事件:

  1. 如果一个库不得不在一遍又一遍的遍历 API 后做出雷同的抉择,它会变得十分轻便。
  2. 咱们不得不创立三个重载,一个是为了解决明确晓得的类型,咱们为每一种类型都写了一个重载(这里一个是 string,一个是 number),一个则是为了通用状况(接管一个 string | number)。而如果减少一种新的类型,重载的数量将呈指数减少。

其实咱们齐全能够用把逻辑写在条件类型中:

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 = createLabel("typescript");
// let a: NameLabel
 
let b = createLabel(2.8);
// let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);
// let c: NameLabel | IdLabel

条件类型束缚(Conditional Type Constraints)

通常,应用条件类型会为咱们提供一些新的信息。正如应用 类型爱护(type guards) 能够 收窄类型(narrowing) 为咱们提供一个更加具体的类型,条件类型的 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>;
// type EmailMessageContents = string

然而,如果咱们想要 MessgeOf 能够传入任何类型,然而当传入的值没有 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 EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;          
// type DogMessageContents = never

true 分支里,TypeScript 会晓得 T 有一个 message 属性。

再举一个例子,咱们写一个 Flatten 类型,用于获取数组元素的类型,当传入的不是数组,则间接返回传入的类型:

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

留神这里应用了索引拜访类型 里的 number 索引,用于获取数组元素的类型。

在条件类型里推断(Inferring Within Conditional Types)

条件类型提供了 infer 关键词,能够从正在比拟的类型中推断类型,而后在 true 分支里援用该推断后果。借助 infer,咱们批改下 Flatten 的实现,不再借助索引拜访类型“手动”的获取进去:

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

这里咱们应用了 infer 关键字申明了一个新的类型变量 Item,而不是像之前在 true 分支里明确的写出如何获取 T 的元素类型,这能够解放咱们,让咱们不必再苦心思考如何从咱们感兴趣的类型构造中挖出须要的类型构造。

咱们也能够应用 infer 关键字写一些有用的 类型帮忙别名(helper type aliases)。举个例子,咱们能够获取一个函数返回的类型:

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[]

当从多重调用签名(就比方重载函数)中推断类型的时候,会依照最初的签名进行推断,因为个别这个签名是用来解决所有状况的签名。

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

散发条件类型(Distributive Conditional Types)

当在泛型中应用条件类型的时候,如果传入一个联结类型,就会变成 散发的(distributive),举个例子:

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

如果咱们在 ToArray 传入一个联结类型,这个条件类型会被利用到联结类型的每个成员:

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

让咱们剖析下 StrArrOrNumArr 里产生了什么,这是咱们传入的类型:

string | number;

接下来遍历联结类型里的成员,相当于:

ToArray<string> | ToArray<number>;

所以最初的后果是:

string[] | number[];

通常这是咱们冀望的行为,如果你要防止这种行为,你能够用方括号包裹 extends 关键字的每一部分。

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

TypeScript 系列

  1. TypeScript 之 Narrowing
  2. TypeScript 之 More on Functions
  3. TypeScript 之 Object Type
  4. TypeScript 之 Generics
  5. TypeScript 之 Keyof Type Operator
  6. TypeScript 之 Typeof Type Operator
  7. TypeScript 之 Indexed Access Types

微信:「mqyqingfeng」,加我进冴羽惟一的读者群。

如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。

退出移动版