TS文档学习 --- 条件类型

353 阅读2分钟

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

很多时候,我们需要基于输入的值来决定输出的值,同样我们也需要基于输入的值的类型来决定输出的值的类型。

条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系。

interface Animal {
  live(): void;
}

interface Dog extends Animal {
  woof(): void;
}
 
// 条件类型的写法有点类似于 JavaScript 中的 三目运算符
type Example1 = Dog extends Animal ? number : string;     
// type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;     
// type Example2 = string
interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}

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

条件类型约束

通常,使用条件类型会为我们提供一些新的信息。正如使用 类型保护(type guards) 可以 收窄类型(narrowing) 为我们提供一个更加具体的类型,条件类型的 true 分支也会进一步约束泛型

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

再举一个例子,我们写一个 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

在条件类型里推断

条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果

我们可以使用 infer 关键字写一些有用的 类型帮助别名(helper type aliases)

// Return 是我们自定义的类型 --- 类型别名
type GetReturnType<Type> = Type extends (...args: unknown[]) => 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

分发条件类型

当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的类型

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

通常这是我们期望的行为,如果你要避免这种行为,你可以用方括号包裹 extends 关键字的每一部分

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// ToArrayNonDist<string | number> --> [ string | number ] --> (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;
// type StrArrOrNumArr = (string | number)[]