TypeScript从类型创建类型之条件类型

39 阅读4分钟

条件类型有助于描述输入输出之间的关系。

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

条件类型看起来和三元表达式很像:

SomeType extends OtherType ? TrueType : FalseType;

extends左边的类型可以分配给右边的类型,那么表达式会返回左边的true分支类型(TrueType);否则将会返回后面的表达式的类型(FalseTypea)

条件类型泛型中使用时是非常强大的。 举个例子,让我们看一个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";
}

这些重载描述了一个JavaScript函数,可以通过传入的参数来选择对应的重载。我们注意到:

  1. 如果使用一个库的api必须去一遍又一遍选择对应的重载时,这会变得非常麻烦。
  2. 我们必须去创建三个重载:其中两个是我们确定的类型(一个string的输入,一个number的输入),并且还有一个通用的情况(兼容string | number)。createLabel可以处理每一种类型。

换种方式,我们可以用条件类型来重构:

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)

通常来说,条件类型的检查会提供给我们新的信息。就像用类型守卫``来收缩类型可以拿到指定的类型一样,条件类型true分支通过我们的类型检查将进一步缩小泛型参数的范围。 举个例子:

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

在这个例子当中,TypeScript发生了错误,因为T没有message属性。我们可以限制T来消除报错:

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

然而,如果我们想让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 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

Flatten给了一个数组类型Flatten使用number类型的索引来取出string[]的元素类型。否则,Flatten只会返回你赋予的类型。

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

我们刚刚在类型约束上使用条件类型来拿到我们想要的类型条件类型提供了我们一种可以在true分支中进行类型推断的关键字infer。举个例子,我们可以在Flatten中推断元素类型代替常用的索引访问类型

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

在这里,我们使用infer关键字在泛型中引入泛型变量Item来代替T拿到类型。

我们可以用infer关键字来写出更加有用的类型别名。举个例子,我们期望拿到函数的返回类型:

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)

条件类型作用于泛型类型时,赋予的联合类型会被分配。举个例子:

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

翻译自Conditional Types