解讀TypeScript中的泛型以及條件類型中的推斷

前言

說一下爲何要寫這篇文章,筆者近三個月內開始接觸並使用 TypeScript 開發項目,一開始總以爲多餘又耗費時間,好在筆者有一個優勢就是算是願意愛折騰愛學習,無論有用沒用~html

近來發現 TypeScript 已成一種趨勢,基本已成大型項目的標配。TypeScript 彌補了弱類型的 JavaScript 所帶來的一些缺點,能夠幫助咱們構建更穩健的代碼,同時也加強可閱讀性和可維護性。可使得許多運行時才能出現的錯誤,在編譯時就暴露出來,讓潛在的問題更容易發現。git

筆者在學習 TypeScript 的過程當中,以爲 TypeScript 比較難以理解和須要花費時間的點,就是泛型以及相關特性了,好比條件推斷 infer 等(固然,也是頗有意思的一部分)。這篇文章就總結並和你們分享一下一些相關知識~github

泛型

TypeScript 中泛型設計的目的是使在成員之間提供有意義的約束,爲代碼增長抽象層和提高可重用性。泛型能夠應用於 Typescript 中的函數(函數參數、函數返回值)、接口和類(類的實例成員、類的方法)。typescript

簡單示例

先來看這個若是日常咱們寫函數的參數和返回值類型可能會這麼寫~約束了函數參數和返回值必須爲數字類型。編程

function identity(arg: number): number {
  return arg;
}
複製代碼

那麼問題來了。若是我要參數和返回值類型限定爲字符串類型的話,又改爲這麼寫。數組

function identity(arg: string): string {
  return arg;
}
複製代碼

不科學呀!當函數想支持多類型參數或返回值的時候,上述寫法將變得十分不靈活。因而泛型就閃亮登場了!安全

考慮如下寫法:ide

function identity<T>(arg: T): T {
  return arg;
}
複製代碼
function identities<T, U>(arg1: T, arg2: U): [T, U] {
  return [arg1, arg2];
}
複製代碼

使用泛型後,能夠接受任意類型,可是又完成了函數參數和返回值的約束關係。十分靈活~可複用性大大加強了!函數式編程

泛型約束

有時候咱們定義的泛型不想過於靈活或者說想繼承某些類等,能夠經過 extends 給泛型加上約束。函數

interface ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
複製代碼

其實泛型咱們在 React 組件裏也很常見(說不定你們以爲很眼熟了),用泛型確保了 React 組件的 Props 和 State 是類型安全的~

interface ICustomToolProps {
  // @TODO
}

interface ICustomToolState {
  // @TODO
}

class CustomTool extends React.Component<ICustomToolProps, ICustomToolState> {
  // @TODO
}
複製代碼

因此你們看上面的 ICustomToolProps、ICustomToolState 其實也是泛型。應用在類上面的泛型語法簡化以下示例:

class Directive<T> {
  private name: T;
  public getName(): T {
    return this.name;
  }
  // @TODO
}
複製代碼

當使用泛型時,通常狀況下經常使用 T、U、V 表示,若是比較複雜,應使用更優語義化的描述,好比上述 React 組件示例。

實踐一下

好比說設計一個指令管理者對象~用來管理指令

enum EDirective {
  Walk = 1,
  Jump = 2,
  Smile = 3
}
class DirectiveManager<T> {
  private directives: Array<T> = [];
  add = (directive: T): Array<T> => {
    this.directives = this.directives.concat(directive);
    return this.directives;
  };
  get = (index: number): T => {
    return this.directives[index];
  };
  shift = (): Array<T> => {
    this.directives = this.directives.slice(1);
    return this.directives;
  };
  // @TODO
}
複製代碼

初始化一個指令管理者的實例。給定泛型爲 number 類型。

能夠發現指令管理者對象成功被限定類型,若是傳參類型錯誤,會被 TypeScript 及時提醒。

瞭解數組方法的泛型

通過上面的介紹,相信你們都對泛型有必定了解了!那麼接下來經過帶你們看 JavaScript 數組方法的泛型來加深理解~

咱們來閱讀如下數組對象的屬性以及方法的泛型(我抽取了一部分,但願你們不要以爲代碼過長,就略過不讀,我以爲也是換一種方式熟悉 JavaScript 語法的一種方式~)

interface Array<T> {
  length: number;
  [n: number]: T;

  reverse(): T[];

  shift(): T;
  pop(): T;

  unshift(...items: T[]): number;
  push(...items: T[]): number;

  slice(start?: number, end?: number): T[];
  sort(compareFn?: (a: T, b: T) => number): T[];
  indexOf(searchElement: T, fromIndex?: number): number;
  lastIndexOf(searchElement: T, fromIndex?: number): number;
  every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
  some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
  forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[]; splice(start: number): T[]; splice(start: number, deleteCount: number, ...items: T[]): T[]; concat<U extends T[]>(...items: U[]): T[]; concat(...items: T[]): T[]; reduce( callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T ): T; reduce<U>( callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U ): U; reduceRight( callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T ): T; reduceRight<U>( callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U ): U; } 複製代碼

相信你們對數組方法都十分熟悉了~下面將帶你們稍微看一下部分方法

shift/pop & push/unshift

shift(): T;
pop(): T;

unshift(...items: T[]): number;
push(...items: T[]): number;
複製代碼

平時你們可能會混淆幾個方法。可是看了它們的函數簽名後,是否以爲一目瞭然。push/unshift 方法調用後返回時數字類型,也就是其數組長度。而 shift/pop 方法調用後返回了彈出的元素,

forEach & map

forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; 複製代碼

這兩個方法很值得一說,由於二者都具有遍歷的特徵,因此常見不少同窗們混用這兩個方法,其實大有講究。看到 forEach 的方法實際上是返回 void 的,而在 map 方法裏,最終是將 T[] 映射成了 U[]。因此呢,一言以蔽之,forEach 通常用來執行反作用的,好比持久的修改一下元素、數組、狀態等,以及打印日誌等,本質上是不純的。而 map 方法用來做爲值的映射,本質上是純淨的,在函數式編程裏十分重要。

concat

splice、concat、reduce、reduceRight 這些方法基本都重載了兩次,也就明顯告訴咱們這些方法是有多種傳參調用方式的。

好比concat<U extends T[]>(...items: U[]): T[];這裏使用到了上述和你們介紹的泛型約束,意思爲能夠傳遞多個數組元素。下面緊跟着的concat(...items: T[]): T[];則告訴咱們也能夠傳遞多個元素。兩個函數簽名都告訴咱們函數返回一個數組,它由被調用的對象中的元素組成,每一個參數的順序依次是該參數的元素(若是參數是數組)或參數自己(若是參數不是數組)。它不會遞歸到嵌套數組參數中。

映射類型

有時候咱們有從舊類型中建立新類型的一個需求場景,TypeScript 提供了映射類型這種方式。 在映射類型裏,新類型以相同的形式去轉換舊類型裏每一個屬性

好比咱們將每一個屬性成爲 readonly 類型,如圖

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

同理以下,見圖可理解~

type Partial<T> = { [P in keyof T]?: T[P] };
複製代碼

那麼你們應該也 get 到下述代碼的意圖了~

type Nullable<T> = { [P in keyof T]: T[P] | null };
複製代碼

擴展一下能夠寫任意的映射類型來知足本身的需求場景~

enum EDirective {
  Walk = 1,
  Jump = 2,
  Smile = 3
}
type DirectiveKeys = keyof typeof EDirective;
type Flags = { [K in DirectiveKeys]: boolean };
複製代碼

type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends string, T> = { [P in K]: T };
複製代碼

條件類型中的推斷

infer 表示在 extends 條件語句中待推斷的類型變量。

在條件類型的 extends 語句中,咱們能夠用 infer 聲明一個類型變量,而後在其分支語句中使用該類型變量。若是不懂,沒有關係,請繼續看下面的例子~

提取函數參數 & 提取函數返回值

該語句中的(param: infer P),爲函數首個參數推斷聲明瞭一個類型變量 P,若是泛型 T 是一個函數,則根據以前的類型變量 P,提取其推斷的函數參數並返回,不然返回原有類型。

type ParamType<T> = T extends (param: infer P) => any ? P : T;
複製代碼

如圖因此,成功提取了 IPrint 的參數類型。

同理以下,提取返回值一樣理解~

type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
複製代碼

提取構造函數參數類型 & 提取實例類型

下述代碼能夠提取構造函數參數類型~

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (
  ...args: infer P
) => any
  ? P
  : never;
複製代碼

T extends new (...args: any[]) => any這裏用到了泛型約束,new (...args: infer P)這一句將參數推斷聲明爲類型變量 P。剩餘的仍是同樣的理解~

下述提取實例類型~(和提取構造函數參數類型小有不一樣~同窗們本身發現一下)

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R
  ? R
  : any;
複製代碼

其餘經常使用的條件推斷

剩餘的列舉一些比較實用的,參照上述方式理解,同窗們如若感興趣,可自行谷歌~

提取數組子元素

type Flatten<T> = T extends (infer U)[] ? U : T;
複製代碼

提取 Promise 值

type Unpromisify<T> = T extends Promise<infer R> ? R : T;
複製代碼

Tuple 轉 Union

type ElementOf<T> = T extends Array<infer E> ? E : never;
複製代碼

Union 轉 Intersection

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (( k: infer I ) => void) ? I : never; 複製代碼

提示

泛型在編譯期間被刪除,所以不要在泛型函數中寫 typeof T、new T、instanceof T。

何時使用泛型?

  1. 當函數、接口、類是接受多類型參數的時候,能夠用泛型提升可重用性。
  2. 當函數、接口、類須要在多個地方用到某個類型的時候。

小結

總的來講,在一箇中大型項目裏採用 TypeScript 目前看來仍是十分有價值的,能夠在開發過程當中給到更多約束,從而大大減小運行時的錯誤。筆者建議你們能夠多多學習 TypeScript,從而寫出更加工整,健壯的代碼。

而此篇文章介紹的泛型和條件推斷可讓你們寫出更加靈活,具備可擴展性的TypeScript類型哈哈哈哈~

以上~謝謝你們的閱讀,如對你們有所助益,不勝榮幸~

參考資料

相關文章
相關標籤/搜索