首頁 > 軟體

TypeScript 泛型推斷實現範例詳解

2022-08-17 14:01:16

前言

最近做東西都在用ts,有時候寫比較複雜的功能,如果不熟悉,型別寫起來還是挺麻煩的。有這樣一個功能,在這裡,我們就不以我們現有的業務來舉例了,我們還是已Animal舉例,來說明場景。通過一個工廠來建立不同的動物範例。在這裡我們藉助泛型來實現型別的約束和動態推到指定型別。

基礎型別準備

  • 用一個列舉來定義Animal的型別
enum EAnimalType {
  dog = 'dog',
  cat = 'cat',
  bird = 'bird',
}
  • 定義不同型別的動物有不同的能力型別
type Dog = {
  /** 大叫 */
  shoutLoudly: () => void;
}
type Cat = {
  say: () => void;
}
type Bird = {
  /** 飛 */
  fly: () => void;
}
  • 定義一個動物的對映型別
 type AnimalMap = {
  [EAnimalType.dog]: Dog;
  [EAnimalType.cat]: Cat;
  [EAnimalType.bird]: Bird;
}

最終使用的方式

/**
 * 定義一個工廠,用來建立具體動物的範例
 * @returns 返回動物的範例
 */
function createAnimalFactory<T extends EAnimalType>(): IAnimal<T> {
  // TODO 根據業務具體實現
  return {} as IAnimal<T>;
}
// 根據泛型建立狗狗的範例
const dog = createAnimalFactory<EAnimalType.dog>();
dog.shoutLoudly();
// 根據泛型建立鳥的範例
const bird = createAnimalFactory<EAnimalType.bird>();
bird.fly()

基於Interface的實現 (失敗了)

  • 接著我們建立一個interface 來定義動物基礎介面
export interface IAnimal<T extends EAnimalType> extends IAnimalExtra<T> {
  id: number;   // 編號
  name: string;   // 名稱
  type: T;   // 型別
}

我們看到IAnimal介面繼承了IAnimalExtra介面,我們想的是通過泛型T來動態推匯出真實的型別。讓我們來看看IAnimalExtra介面怎麼寫

  • IAnimalExtra介面
export type IAnimalExtra<T extends EAnimalType>  {
  [c in keyof AnimalMap[T]]: AnimalMap[T][c];
}

我們這樣寫,發現偵錯控制檯報了很多錯,具體分析了下錯誤,介面不支援這種功能。接著我們嘗試,改成type試一下。

  • 最後用type 去替代 IAnimalExtra
export type IAnimalExtra<T extends EAnimalType> = {
  [c in keyof AnimalMap[T]]: AnimalMap[T][c];
}

我們用type,果然不不錯了,證明我們的思路是對的。乍一看,寫的怎麼複雜[c in keyof AnimalMap[T]]: AnimalMap[T][c]; 不要怕,我們先具體分析一下這段程式碼,就很好理解了。

  • 先看AnimalMap[T],可以理解從AnimalMap型別中獲取對應的型別,近似js中從物件取值
  • keyof 接受一個Object,生成Object的key的字串的union(聯合)
  • in 可以遍歷列舉型別,類似 for...in

整體的功能就是根據泛型T,獲取AnimalMap中的某個型別,遍歷。之後我們專門寫篇文章,介紹下這塊相關的內容。

  • extends IAnimalExtra<T> 報錯了

在我們最終認為可以的情況下,發現有報錯了,內容為【介面只能擴充套件物件型別或物件型別與靜態已知成員的交集】

所有內容都基於type 實現

在我們嘗試了多次之後,發現Interface怎麼也滿足不了需求,接著我們都換成type去試試。

export type IAnimal<T extends EAnimalType> = IAnimalExtra<T> & {
  id: number;   // 編號
  name: string;   // 名稱
  type: T;   // 型別
}

這裡我們用了&交叉型別類合併介面的型別。

換成type之後,已能完全滿足我們的需求,能根據泛型推斷出我們想要的型別。

完整Demo

/**
 * 動物列舉
 */
export enum EAnimalType {
  dog = 'dog',
  cat = 'cat',
  bird = 'bird',
}
type Dog = {
  /** 大叫 */
  shoutLoudly: () => void;
}
type Cat = {
  say: () => void;
}
type Bird = {
  /** 飛 */
  fly: () => void;
}
export type AnimalMap = {
  [EAnimalType.dog]: Dog;
  [EAnimalType.cat]: Cat;
  [EAnimalType.bird]: Bird;
}
export type IAnimalExtra<T extends EAnimalType> = {
  [c in keyof AnimalMap[T]]: AnimalMap[T][c];
}
export type IAnimal<T extends EAnimalType> = IAnimalExtra<T> & {
  id: number;   // 編號
  name: string;   // 名稱
  type: T;   // 型別
}
/**
 * 定義一個工廠,用來建立具體動物的範例
 * @returns 返回動物的範例
 */
function createAnimalFactory<T extends EAnimalType>(): IAnimal<T> {
  // TODO 根據業務具體實現
  return {} as IAnimal<T>;
}
// 根據泛型建立狗狗的範例
const dog = createAnimalFactory<EAnimalType.dog>();
dog.shoutLoudly();
// 根據泛型建立鳥的範例
const bird = createAnimalFactory<EAnimalType.bird>();
bird.fly();

結束語

最近深度使用ts中,有一些感觸,用好型別,前期看著比較費時,但隨著專案的迭代,業務的複雜,對我們後期幫助還是很大的。小夥伴,你們在專案中用ts了嗎?

以上就是TypeScript 泛型推斷實現範例詳解的詳細內容,更多關於TypeScript 泛型推斷的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com