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

泛型

TypeScript 中泛型設(shè)計的目的是使在成員之間提供有意義的約束,為代碼增加抽象層和提升可重用性。泛型可以應(yīng)用于 Typescript 中的函數(shù)(函數(shù)參數(shù)、函數(shù)返回值)、接口和類(類的實例成員、類的方法)。

簡單示例

先來看這個如果平常我們寫函數(shù)的參數(shù)和返回值類型可能會這么寫~約束了函數(shù)參數(shù)和返回值必須為數(shù)字類型。

function identity(arg: number): number {
  return arg;
}

那么問題來了。如果我要參數(shù)和返回值類型限定為字符串類型的話,又改成這么寫。

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

不科學(xué)呀!當(dāng)函數(shù)想支持多類型參數(shù)或返回值的時候,上述寫法將變得十分不靈活。于是泛型就閃亮登場了!

考慮以下寫法:

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

使用泛型后,可以接受任意類型,但是又完成了函數(shù)參數(shù)和返回值的約束關(guān)系。十分靈活~可復(fù)用性大大增強(qiáng)了!

泛型約束

有時候我們定義的泛型不想過于靈活或者說想繼承某些類等,可以通過 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 其實也是泛型。應(yīng)用在類上面的泛型語法簡化如下示例:

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

當(dāng)使用泛型時,一般情況下常用 T、U、V 表示,如果比較復(fù)雜,應(yīng)使用更優(yōu)語義化的描述,比如上述 React 組件示例。

實踐一下

比如說設(shè)計一個指令管理者對象~用來管理指令

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 類型。


微信圖片_20201113105046.jpg

可以發(fā)現(xiàn)指令管理者對象成功被限定類型,如果傳參類型錯誤,會被 TypeScript 及時提醒。

了解數(shù)組方法的泛型

經(jīng)過上面的介紹,相信大家都對泛型有一定了解了!那么接下來通過帶大家看 JavaScript 數(shù)組方法的泛型來加深理解~

我們來閱讀以下數(shù)組對象的屬性以及方法的泛型(我抽取了一部分,希望大家不要覺得代碼過長,就略過不讀,我覺得也是換一種方式熟悉 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;
}

相信大家對數(shù)組方法都十分熟悉了~下面將帶大家稍微看一下部分方法

shift/pop & push/unshift

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

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

平時大家可能會混淆幾個方法。但是看了它們的函數(shù)簽名后,是否覺得一目了然。push/unshift 方法調(diào)用后返回時數(shù)字類型,也就是其數(shù)組長度。而 shift/pop 方法調(diào)用后返回了彈出的元素,

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[];

這兩個方法很值得一說,因為兩者都具備遍歷的特征,所以常見很多同學(xué)們混用這兩個方法,其實大有講究??吹?forEach 的方法其實是返回 void 的,而在map 方法里,最終是將 T[] 映射成了 U[]。所以呢,一言以蔽之,forEach 一般用來執(zhí)行副作用的,比如持久的修改一下元素、數(shù)組、狀態(tài)等,以及打印日志等,本質(zhì)上是不純的。而 map 方法用來作為值的映射,本質(zhì)上是純凈的,在函數(shù)式編程里十分重要。

concat

spliceconcat、reducereduceRight 這些方法基本都重載了兩次,也就明顯告訴我們這些方法是有多種傳參調(diào)用方式的。

比如concat<U extends T[]>(...items: U[]): T[];這里使用到了上述和大家介紹的泛型約束,意思為可以傳遞多個數(shù)組元素。下面緊跟著的concat(...items: T[]): T[];則告訴我們也可以傳遞多個元素。兩個函數(shù)簽名都告訴我們函數(shù)返回一個數(shù)組,它由被調(diào)用的對象中的元素組成,每個參數(shù)的順序依次是該參數(shù)的元素(如果參數(shù)是數(shù)組)或參數(shù)本身(如果參數(shù)不是數(shù)組)。它不會遞歸到嵌套數(shù)組參數(shù)中。

映射類型

有時候我們有從舊類型中創(chuàng)建新類型的一個需求場景,TypeScript 提供了映射類型這種方式。 在映射類型里,新類型以相同的形式去轉(zhuǎn)換舊類型里每個屬性

比如我們將每個屬性成為 readonly 類型,如下

type Readonly<T> = { readonly [P in keyof T]: T[P] };
2.jpg

同理如下,見圖可理解~

type Partial<T> = { [P in keyof T]?: T[P] };
3.jpg

那么大家應(yīng)該也 get 到下述代碼的意圖了~

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

擴(kuò)展一下可以寫任意的映射類型來滿足自己的需求場景~

enum EDirective {
  Walk = 1,
  Jump = 2,
  Smile = 3
}
type DirectiveKeys = keyof typeof EDirective;
type Flags = { [K in DirectiveKeys]: boolean };
4.jpg
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 聲明一個類型變量,然后在其分支語句中使用該類型變量。如果不懂,沒有關(guān)系,請繼續(xù)看下面的例子~

提取函數(shù)參數(shù) & 提取函數(shù)返回值

該語句中的(param: infer P),為函數(shù)首個參數(shù)推斷聲明了一個類型變量 P,如果泛型 T 是一個函數(shù),則根據(jù)之前的類型變量 P,提取其推斷的函數(shù)參數(shù)并返回,否則返回原有類型。

type ParamType<T> = T extends (param: infer P) => any ? P : T;
5.jpg

如圖所以,成功提取了 IPrint 的參數(shù)類型。

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

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

提取構(gòu)造函數(shù)參數(shù)類型 & 提取實例類型

下述代碼可以提取構(gòu)造函數(shù)參數(shù)類型~

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

6.jpg

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

下述提取實例類型(和提取構(gòu)造函數(shù)參數(shù)類型小有不同同學(xué)們自己發(fā)現(xiàn)一下)

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

其他常用的條件推斷

剩余的列舉一些比較實用的,參照上述方式理解,同學(xué)們?nèi)缛舾信d趣,可自行谷歌~

提取數(shù)組子元素

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

提取Promise值

type Unpromisify<T> = T extends Promise<infer R> ? R : T;

Tuple 轉(zhuǎn) Union

type ElementOf<T> = T extends Array<infer E> ? E : never;

Union 轉(zhuǎn) Intersection

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

什么時候使用泛型

1.當(dāng)函數(shù)、接口、類是接受多類型參數(shù)的時候,可以用泛型提高可重用性。
2.當(dāng)函數(shù)、接口、類需要在多個地方用到某個類型的時候。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容