TypeScript 官方手冊翻譯計劃【九】:型別操控-條件型別

Chor發表於2021-12-04
  • 說明:目前網上沒有 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)[]   

相關文章