关于前端:TypeScript-官方手册翻译计划九类型操控条件类型

37次阅读

共计 4362 个字符,预计需要花费 11 分钟才能阅读完成。

  • 阐明 :目前网上没有 TypeScript 最新官网文档的中文翻译,所以有了这么一个翻译打算。因为我也是 TypeScript 的初学者,所以无奈保障翻译百分之百精确,若有谬误,欢送评论区指出;
  • 翻译内容 :暂定翻译内容为 TypeScript Handbook,后续有空会补充翻译文档的其它局部;
  • 我的项目地址 :TypeScript-Doc-Zh,如果对你有帮忙,能够点一个 star ~

本章节官网文档地址:Conditional Types

条件类型

在大多数利用的外围中,咱们须要基于输出决定执行哪一个逻辑。JavaScript 利用也是如此,但因为值很容易自省(译者注:自省指的是代码可能自我查看、拜访外部属性,取得代码的底层信息),所以具体要执行哪个逻辑也得看输出数据的类型。条件类型就能够用于形容输出类型和输入类型之间的分割。

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 中的条件表达式( 条件 ? 真分支: 假分支 ):

SomeType extends OtherType ? TrueType : FalseType;

extends 右边的类型能够赋值给左边的类型时,最终失去的就是第一个分支(真分支)中的类型,否则失去第二个分支(假分支)中的类型。

仅从下面的例子来看,条件类型看起来并不是很有用 —— 就算不依附它,咱们本人也能晓得 Dog extends Animal 是否成立,而后抉择对应的 number 类型或者 string 类型!但如果把条件类型和泛型联合应用,那它就能施展微小的威力了。

举个例子,咱们看看上面的 createLabel 函数:

interface IdLabel {id: number            /* 一些属性 */}
interface NameLabel {name: string          /* 其它属性 */}
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. 咱们须要创立三个重载:前两个别离针对具体的输出类型(stringnumber),最初一个则针对最通用的状况(输出类型为 string | number )。一旦 createLabel 减少了可能解决的新类型,那么重载的数量将以指数模式减少。

所以无妨换一种形式,咱们能够将下面代码的逻辑编码到一个条件类型中:

type NameOrId<T extends number | string> = T extends nummber
? 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 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<Emial>;
            ^
          // 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 EmialMessageContents = MessageOf<Email>;
            ^
          // type EmialMessageContents = string
type DogMessageContents = MessageOf<Dog>;
             ^
          // type DogMessageContents = never                

在条件类型的真分支中,TypeScript 晓得 T 将会有 message 属性。

再来看一个例子。咱们能够编写一个 Flatten 函数,它能够将数组类型扁平化为数组中元素的类型,对于非数组类型则保留其原类型:

type Flatten<T> = T extends any[] ? T[number] : T;

// 提取元素类型
type Str = Flatten<string[]>;
      ^
    // type Str = string

// 保留原类型          
type Num = Flatten<number>;
      ^
    // type Num = number      

Flatten 承受数组类型的时候,它会应用 number 按索引拜访,从而提取出数组类型 string[] 中的元素类型;如果它承受的不是数组类型,则间接返回给定的原类型。

在条件类型中进行推断

在下面的例子中,咱们应用条件类型去利用束缚并提取出类型。因为这种操作很常见,所以条件类型提供了一种更简略的形式来实现。

条件类型提供了 infer 关键字,让咱们能够推断出条件中的某个类型,并利用到真分支中。举个例子,在下面的 Flatten 函数中,咱们能够间接推断出数组元素的类型,而不是通过索引拜访“手动”提取出元素的类型:

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

这里,咱们应用 infer 关键字申明式地引入了一个新的泛型类型变量 Item,而不是在真分支中指定如何提取出 T 数组的元素类型。这使咱们不用再去思考如何找出咱们感兴趣的类型的构造。

咱们能够应用 infer 关键字编写一些有用的工具类型别名。举个例子,在一些简略的状况下,咱们能够从函数类型中提取出返回值的类型:

type GetReturnType<Type> = Type extends (...args: never[]) => infer Rerturn
? 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 传入一个联结类型,那么条件类型将会利用给联结类型的每一个成员。

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' 不再是一个联结类型
type StrArrOrNumArr = ToArrayNonDist<string | number>;
          ^
        // type StrArrOrNumArr = (string | number)[]   

正文完
 0