TypeScript 基礎學習之泛型和 extends 關鍵字

泛型

A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

我們工作的大部分內容是構建組件,定義有效、一致並且可複用的API很重要。組件能夠處理當前的數據,又能考慮兼容很多未來的數據,這樣的組件能提高工作效率,也能在構建軟件系統的時候提供非常靈活的能力。

基本使用

通過泛型可以定義通用的數據結構,增加 TypeScript 代碼中類型的通用性。

  • 處理函數

先看一個具體的例子,感受一下泛型的應用。

首先定一個 log 函數,功能很簡單把傳入的參數直接 return 就行,函數參數類型是 string,那麼返回值也是 string 類型。

function log(arg: string): string {
    return arg;
}

當其他地方也想使用這個函數,但是參數入參數類型是 number,這個時候我們也許可以這麼做:

function log(arg: string | number): string | number {
    return arg;
}

當有更多的地方要使用這個函數的時候,那這個函數的參數類型定義和返回值類型定義將會變得無比冗長,或者可能就直接使用 any 來解決,當使用 any 的時候就失去了使用 TS 最初的初心了。

這個時候泛型出現了,能解決輸入輸出一致的問題,我們可以這樣寫:

function log<T>(arg: T): T {
    return arg;
}

這個 log 函數通過泛型來約束輸入輸出一致性的問題,把動態的泛型類型拋給函數的使用者,我們只需要保證輸入輸出的一致性就可以,還能支持任何類型。泛型中的 T 就像一個佔位符,或者說一個變量,在使用的時候把定義的類型像參數一樣傳入就可以了。

我們在使用的時候可以有兩種方式指定類型。第一,是直接定義要使用的類型,第二,是默認 TS 的類型推斷,TS自動推導出要傳入的類型:

log<string>('log')  // 定義 T 爲 string

print('log')  // TS 的類型推斷,自動推導類型爲 T 的類型爲 string
  • 默認參數

在 JS 中對於一個函數入參,可以使用默認參數來簡化當沒有傳參數時候的默認值,在 TS 中我們可以這樣使用:

function log<T = string>(arg: T): T {
    return arg;
}

當沒有傳泛型參數的時候 T 的默認類型是 string 類型,就如果 JS 中的函數默認參數類似的用法

  • 多個參數

當函數中有多個參數的時候可以這樣使用:


function log<T, U>(type: T, info: U): [T, U] {
    return [type, info];
}

通過在泛型定義多個對應位置的類型就可以獲取到相應的泛型傳參來對輸入輸出做一些處理。

  • 函數返回值

泛型不僅可以很方便地約束函數的參數類型,還可以用在函數執行副作用操作的時候。發送請求是我們使用的很多的操作,我們會有一個通用的發送請求的異步方法,請求不同的 url 會返回不同的類型數據,那麼我們可以這樣使用。

function request<T>(url: string): Promise<T> {
    return fetch(url).then(res => res.json())
}

interface IUserInfo {
  name: string;
  age: number;
  avatar: string;
  gender: 'male' | 'female';
  city: string;
}

request<IUserInfo>('/getuserinfo').then(res => {
  console.log(res)
});

這個時候返回的數據 TS 就會識別出 res 的類型,對解下來的代碼編寫會有很大幫助。

應用

上面的一些小例子,我們對泛型有了一些瞭解,在平時我們可以這樣使用。

  • 泛型約束類

定一個棧,有出棧和入棧兩個方法,規定了出棧和入棧的元素類型必須一致。


class Stack<T> {
  private data: T[] = []

  push(item:T) {
      return this.data.push(item)
  }

  pop(): T | undefined {
      return this.data.pop()
  }
}

在使用的使用傳入類型生成實例,在調用對應的出棧和入棧方法的時候入參數類型不對就會報錯,從而約束了棧的元素類型。


const test1 = new Stack<number>()
const test2 = new Stack<string>()

  • 泛型約束接口

function request<T>(url: string): Promise<T> {
    return fetch(url).then(res => res.json())
}

interface IUserInfo<T> {
  name: string;
  age: number;
  avatar: string;
  gender: 'male' | 'female';
  address: T;
}

request<IUserInfo<string>>('/getuserinfo').then(res => {
  console.log(res)
});

rInfo 的 address 的類型就是 string,讓 interface 更靈活。

  • Pick

Pck 是 TS 內置的函數,作用是挑選出對象類型 T 中 U 對應的屬性和類型,創建一個新的對象類型。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

interface IUserInfo {
  name: string;
  age: number;
  avatar: string;
  gender: 'male' | 'female';
}

type Test = Pick<IUserInfo, 'name'> // { name: string }
  • Omit

與Pick的功能是互補的,挑選出對象類型 T 中不在 U 中的屬性和類型,創建一個新的對象類型。


type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

interface IUserInfo {
  name: string;
  age: number;
  avatar: string;
  gender: 'male' | 'female';
}

type Test = Omit<IUserInfo, 'name' | 'avatar' | 'gender'> // { age: number }

總結

泛型,從字面上來理解,就是一般的,廣泛的,具有通用性的。
泛型是指在定義函數、接口或類的時候,不預先指定具體類型,而是在使用的時候再指定類型。

泛型中的 T 就像一個佔位符、或者說一個變量,在使用的時候可以把定義的類型像參數一樣傳入,它可以原封不動地輸出。

extends

本文主要整理 extends 關鍵字在 typescript中的相關用法,平時在看一些複雜的 TS 類型的時候經常會看到使用 extends 這個關鍵字

繼承類型

TS 中的 extends 關鍵字第一個用法可以理解成 JS 中相似的用法繼承類型。

interface IName {
  name: string;
}

interface IGender {
  gender: string;
}

interface IPerson extends IName, IGender {
  age: number;
}

const corgi: IPerson = {
  name: 'corgi',
  gender: 'female',
  age: 18,
}

以上示例中,IName 和 IGender 兩個接口,分別定義了 name 屬性和 gender 屬性,IPerson 則使用extends 關鍵字多重繼承的方式,繼承了 IName 和 IGender,同時定義了自己的屬性age,此時 IPerson 除了自己的屬性外,還同時繼承了 IName 和 IGender 的屬性。

條件判斷

When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).

當 extends 左邊的類型可以賦值給右邊的類型時,你會在第一個分支中獲取獲得這個類型(true),否你會在第二個個分支中獲得這個類型(false)

普通條件類型

先來直接看個例子

// 示例1
interface IAnimal {
  name: string;
}

interface IDog extends IAnimal {
  color: string;
}

// A的類型爲string
type Test = IDog extends IAnimal ? string : number;

extends 的條件判斷和 JS 中的三元表達式很類似,如果問號前面的條件爲真就把 string 類型賦值給 A,否則就把 number 類型賦值給 A。那麼問號前面的條件判斷真假的邏輯是什麼呢?就像上面的那段英文描述一樣,當extends 左邊的類型可以賦值給右邊的類型的時候,就會真,否則爲假。

在上面的例子中,IDog 是 IAnimal 的子類,子類比父類的限制更多,如果能滿足子類的條件約束,就一定能滿足父類的條件約束,IDog 類型的值可以滿足 IAnimal 類型,判斷結果爲真,Test 的類型爲 string。

再來一個例子:


// 示例2
interface I1 {
  name: string
}

interface I2 {
  name: string
  age: number
}
// A的類型爲string
type Test = I2 extends I1 ? string : number

這個例子,代入上面的解法來看就是,能滿足 I2 類型約束的值也滿足 I1 類型約束,判斷結果爲真,Test 的類型爲 string。

多看幾個例子:

type Test1 = 'x' extends 'x' ? "true" : "false";  // "true"
type Test2 = 'x' extends 'y' ? "true" : "false"  // "false"
type Test3 = 100 extends 100 ? "true" : "false"  // "true"
type Test4 = 200 extends 100 ? "true" : "false"  // "false"
type Test5 = {} extends {name:string} ? "true" : "false"  // "false"
type Test6 = {name:string} extends {} ? "true" : "false"  // "true"

按照上面的解釋能夠很好的解釋出最後的結果。

分配條件類型

再多看幾個例子:

type Test1 = 'x' extends 'x' ? string : number; // string
type Test2 = 'x' | 'y' extends 'x' ? string : number; // number

type P<T> = T extends 'x' ? string : number;
type Test3 = P<'x' | 'y'> // tring | number

type P<T> = 'x' extends T ? string : number;
type Test4 = P<'x' | 'y'> // tring

這裏就先把最後的結果直接給出來了,看到 Test1、Test2 和 Test4 還能理解,但是 Test3 的結果爲什麼就是 string |nunber這個類型了呢?同樣的按照泛型傳參數,按照直覺來說,Test3 和 Test2 應該是一樣的結果,爲什麼結果差異這麼大呢?

這裏導致結果和直覺不一樣的原因就是所謂的分配條件類型。

When conditional types act on a generic type, they become distributive when given a union type

當 extends 前面的參數是一個泛型類型,當傳入的參數是一個聯合類型的時候,就是使用分配律計算最後的結果,分配律就是我們從數學中學到的分配律。把聯合類型中的每個類型代入條件判斷得到每個類型的結果,再把每個類型的結果聯合起來,得到最後的類型結果。

那麼就可以按照這個解法來代入 Test3 的解釋原因:
extends 前面的參數 T 是泛型參數,Test3 中 泛型代入的的 x | y這個聯合類型,這個時候就觸發了分配條件類型,來使用分配律

'x' extends 'x' ? string : number; // string
'y' extends 'x' ? string : number; // number
type Test3 = string | number

按照分配條件來看最後的結果恍然大悟,總之要觸發分配條件類型要滿足兩個條件,第一,extends 前面的參數是泛型類型,第二,參數是聯合類型。

條件分配類型是系統默認的行爲,那麼在某些需求不想要出發條件分配類型應該怎麼辦呢?

看下面的例子:

type P<T> = [T] extends ['x'] ? string : number;
type Test = P<'x' | 'y'> // number

這個使用使用了[]這個符號把泛型類型參數包起來,這個時候 extends 前面的參數就變成這個樣子['x' | 'y'],不滿足觸發分配條件類型的條件,按照普通條件來判斷,得到最後的結果爲 number。

never

來個例子看看:

// never是所有類型的子類型
type Test1 = never extends 'x' ? string : number; // string

type P<T> = T extends 'x' ? string : number;
type Test2 = P<never> // never

上面直接給出了最後的結果,但是爲什麼看起來 Test2 最後的結果又和直覺中不太一樣,never 不是聯合類型,直接代入條件類型之後,按理來說 Test2 和 Test1 的結果應該一樣纔對。

事實上,never 被認爲是空的聯合類型,也就是沒有任何項的聯合類型,所以還是滿足上面的分配條件類型,因爲沒有任何聯合項可以分配,所以P<T>根本就沒有執行,就和永遠沒有返回的函數一樣,屬於 never 類型。

按照上面的條件,可以這樣子來阻止分配條件類型。

type P<T> = [T] extends ['x'] ? string : number;
type Test = P<never> // string

應用

Exclude

type Exclude<T, U> = T extends U ? never : T;

Exclude 是 TS 內置的應用方法,作用是從第一個聯合類型參數 T 中,把第二個聯合類型參數 U 出現的聯合項去掉。

type Test = Exclude<'A' | 'B', 'A'> // 'B'

其實就是應用了分配條件類型:

type Test = Exclude<'A', 'A'> | type Test = Exclude<'B', 'A'>
type Test = 'A' extends 'A' ? never : 'A' | 'B' extends 'A' ? never : 'B'
type Test = never | 'B'
type Test = 'B'

Extract

type Exclude<T, U> = T extends U ? T : never;

Extract 是 TS 內置的應用方法,作用是從第二個聯合類型參數 U 中,把第一個聯合類型參數 T 出現的聯合項提取出來。

type Test = Exclude<'A' | 'B', 'A'> // 'A'

其實就是應用了分配條件類型。

type Test = Exclude<'A', 'A'> | type Test = Exclude<'B', 'A'>
type Test = 'A' extends 'A' ? 'A' : never | 'B' extends 'A' ? 'B' : never
type Test = 'A' | never
type Test = 'A'

總結

在 typescript 中的 extends 關鍵字主要用法,就是繼承類型,結合三元表達式來完成更多的類似函數的應用方法,在三元表達式中還要注意分配條件類型的應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章