複雜場景下的 typescript 類型錨定 (2) ----- 泛型與類型推導

做者:東墨

前言:在編寫 typescript 應用的時候,有時候咱們會但願複用或者構造一些特定結構的類型,這些類型只從 typescript 靠內建類型和 interface、class 比較難以表達,這時候咱們就須要用到類型推導, 而討論類型推導, 則離不開泛型推斷(#infer), 本文咱們只討論泛型html


上一篇typescript


泛型


從形式上看, typescript 中的泛型如同大多數語言(不包括還沒有實現的 Go :P)裏的泛型:數組


// one constrain for array-like object
interface ArrayLike<T> {
    readonly length: number;
    readonly [n: number]: T;
}複製代碼


上面的 ArrayLike 表達了類數組結構, 它表明的對象的特徵是:bash


  1. 有隻讀的 length 字段;
  2. 其它字段必須是數字, 且每一個字段對應的值類型必須爲 T, 這裏的 T 就是泛型標記


array-like 對象是 Javascript 中很古老的對象, 如聲明爲 function (...) {...} 格式的函數, 其內部的 arguments 變量就是典型的 array-like 對象.app


泛型每每能夠用在這些場景:函數


  • 保留字 interface
  • 保留字 type
  • 命名空間(#namespace) 中的 class(實際上此時它是一個 interface)
  • 運行時中的 class
  • 函數定義(包括 class 的構造函數)


接下來咱們依次說明, 在這些場景中, 類型推導如何發揮它的威力.學習


基於泛型的類型推導


在利用泛型作類型推導時, 切記:ui


  1. 泛型服務於類型的靜態分析, 不服務於 Javascript 運行時
  2. 在考慮類型推導時的邏輯推算, 應考慮"它在運行時會獲得何種類型", 而應考慮"基於類型自己的特性會獲得何種類型"


第 2 點可能有點難以理解, 咱們先略過, 在下一篇, 咱們會明白這句話的含義.es5


interface: T 與 keyof


全鍵可選化


上一篇層提到, 咱們能夠經過 keyof 提取一個 interface 的全部鍵名, 當引入泛型後, keyof 還能夠作更有趣的事情:spa


/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};複製代碼


Partial 的做用是: 對 T(T 須要是可被當作 interface 的類型), 求其全部的鍵名, 並依賴鍵名, 獲得一個結構徹底相同, 但其全部鍵均可選 的新 interface.


好比, 對與 Form, UninitForm 能夠是它的全鍵可選項版本:


interface Form {
    name: string
    age: number
    sex: 'male' | 'female' | 'other'
}

type UninitForm = Partial<Form>複製代碼


則 UninitForm 等價於:


{
    name?: string;
    age?: number;
    sex?: "male" | "female" | "other";
}複製代碼


全鍵必需化


相反, 若是你已經有了 UninitForm, 則你能夠得靠 Required 到它的全鍵必需化版本:


/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};複製代碼


interface UninitForm {
    name?: string;
    age?: number;
    sex?: "male" | "female" | "other";
}

type AllKeyRequriedForm = Required<UninitForm>複製代碼


全鍵只讀化


結合 readonly, 咱們能夠把一個 interface 裏全部的鍵轉化爲只讀


/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};複製代碼


強大的 extends 三元推導


對於 js 而言, extends 是擴展類的保留字; 而在 typescript 中, 當 extends 出現的以下的場景時, 它意味着類型推導:


/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;複製代碼


形如 T extends [DEP] ? [RESULT1] : [RESULT2] 的表達式, 是 typescript 中的一種類型推導式, 它的規則是:


若泛型 T 必須知足 [DEP] 的約束(即 T extends [DEP]true), 則表達式結果爲 [RESULT1]; 反之表達式結果爲 [RESULT2]:


  1. 當 [DEP] 是基本類型時, 若是 T 是對應的基本類型, 則 T extends [DEP]true, 反之爲 false
  2. 當 [DEP] 是 interface/class 時, 若是 T 必須知足它的約束, 則 T extends [DEP]true, 反之爲 false
  3. 當 [DEP] 是 void/never 時, 按基本類型處理
  4. 當 [DEP] 是 聯合類型, 組成 [DEP] 的類型會依次代入 T 進行運算, 最終的結果是這些運算結果的聯合類型
  5. 當 [DEP] 是 any, 則 T extends [DEP] 恆爲 true


按照這些規則, 咱們來分析一下這個 Exclude.


/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;複製代碼


分析可知:


  • Exclude 中有兩個必要的泛型標記 TU(由於它們都未提供默認泛型)
  • 若是 T 是聯合類型, 則咱們會獲得 T 中除了 U 以外的全部類型


咱們來應用一下 Exclude


type NoOne = Exclude<1 | 2 | 3, 1>  // NoOne = 2 | 3複製代碼


另外, 爲了說明上面的第 4 點, 咱們能夠寫一個毫無用處的 NotRealExclude;


type NotRealExclude<T, U> = T extends U ? U : T;

type Orig = NotRealExclude<1 | 2 | 3, 1> // Orig = 1 | 2 | 3複製代碼


因爲 NotRealExclude 在 T 不符合 U 的時候返回 U, 而在 T 符合 U 的時候又返回 T, 最終的結果是: 組成 T 的全部類型又被從新組裝了起來.


extends 亂燉


在瞭解了 extends 的基本用法後, 咱們來看更多的例子:


挑選, 排除, 重組對象的鍵值


/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};複製代碼


正如咱們經常使用的 pick 函數 能夠提取對象中特定的鍵值對, Pick 也能夠提取 T 中特定的鍵值定義.


interface Person {
    name: string
    age: number
    sex?: "male" | "female" | "other";
}

/**
 * equivalent to { name: string }
 */
type SimplePersonInfo = Pick<Person, 'name'>複製代碼


既然能夠 Pick, 那也能夠 Omit


/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;複製代碼


/**
 * equivalent to { name: string, age: number }
 */
type SimplePersonInfo = Omit<Person, 'sex'>複製代碼


咱們知道, 在對象的索引中, in 關鍵字能夠用於從聯合類型中提取類型, 做爲 interface 的鍵名, 好比:


type Ks = 'a' | 'b' | 'c'
type KObject = {
    [P in Ks]: any
}複製代碼


則 KObject 能夠包含 'a', 'b', 'c' 三種類型的鍵名.


結合 extends, 咱們能夠輕鬆地從一個 interface 構建具備一樣類型的值的字典, 這就是 Record:


/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};複製代碼


// construct one family with 3 keys: parent, mom, child
type Family = Record<'parent' | 'mom' | 'child', Person>複製代碼


這等價於


interface Family {
    parent: Person,
    mom: Person,
    child: Person
}複製代碼


非 Null 化變量等


/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;複製代碼


總結


至此,


  1. 咱們知道了如何利用泛型推出新的類型
  2. 咱們知道了如何將泛型與 extends 三元組, 索引 in 表達式 結合, 對 interface 進行拆解、重組


但到目前爲止, 咱們處理的場景都限與非函數的 interface(class)/type, 對於函數的 interface, 咱們可否進行一些特殊處理, 好比, 對一個已有的函數定義, 提取其第 2 個參數的參數類型? 對於下面這個 func, 咱們可否提取出 arg2 的類型?


interface func () {
    (arg1: string, arg2: {
        bar: string
    }): void
}複製代碼


下一篇咱們會討論, 如何使用 infer 關鍵字達成這一目標.


其它


基於泛型的類型推導, 理論上從 typescript 2.8 開始(實際上更早, 但 typescript 2.8/3.5 是具備里程碑意義的版本, 故以此劃分)就能夠實現了, 從 typescript 3.5, 官方內置了一些用於推導的的類型(#type)和接口(#interface), 這些是咱們用於學習類型推導的良好案例. 本文用到的全部例子, 都來自於 typescript 內置的 lib.es5.d.ts.


注意 對於泛型, T 每每是用做泛型標記的第一個選擇, T 之於泛型, 比如 foo 之於樣例代碼

相關文章
相關標籤/搜索