記錄寫一個 TS 類型的思考優化過程

某一天,一個朋友忽然在微信裏問我,「['TYPE'] => { 'TYPE': 'TYPE' },寫個函數轉換這個,類型怎麼加?」。筆者立刻的回覆就是,「用 reduce 將數組轉對象,TS 類型寫個泛型便可」。html

說是這麼說,可是發如今寫 TS 類型上實際上卻的確不是那麼容易,因而,筆者立刻偷偷開始嘗試。(推薦你們能夠在 TypeScript Playground 上練習 TS 類型)typescript

首先轉換從數組轉換到對象,咱們用 reduce 很容易達成目標:數組

function convert(source) {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {});
}
複製代碼

但本文的重點是寫這個函數的類型。簡而言之,就是須要寫 convert 函數參數的類型和返回值類型,從而能夠定義函數的形狀。微信

因而,筆者折騰了一會,寫出來下述類型,編輯器

function convert<K extends string>(source: K[]): Record<K, K> {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}
type TNAME = 'LIN' | 'HUA';

const peoples: TNAME[] = ['LIN', 'HUA'];
const peopleMap = convert<TNAME>(peoples);
function getNameFromPeopleMap<T extends any>(name: T): T {
  return peopleMap[name];
}

const t = getNameFromPeopleMap('HUA');
複製代碼


新增一個 getNameFromPeopleMap 函數,編寫泛型 (name: T): T。從而達成了如下效果:
函數

image.png


發給朋友,可是立刻被吐槽了,首先 LIN、HUA 兩個值就在代碼上被重複了兩次,TNAME 類型略顯冗餘。其次,上述類型只是定義了函數傳參值和返回值相同,並無達到預期中約束傳參值的效果。

因此傳入任意值,編輯器都不會提示類型錯誤。
優化

image.png

所以,上述類型若是打個分,連及格線都不達。因而,進行考慮優化。爲了解決上述兩個問題,一段時間後,新的類型火熱出爐!ui

function convert<K extends string>(source: readonly K[]): Record<K, K> {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}

const peoples = ['LIN', 'HUA'] as const;
const peopleMap = convert<typeof peoples[number]>(peoples);
function getNameFromPeopleMap<T extends typeof peoples[number]>(name: T): T {
  return peopleMap[name] as any;
}

const t = getNameFromPeopleMap('LIN');
複製代碼

爲了減小冗餘的 TNAME。使用了 TypeScript 3.4 版本新增的 const 斷言。它能夠將咱們一個字面量表達式斷言爲一個 TS 類型。以下:spa

// Type '"HELLO"'
const STR = "HELLO" as const;

// Type 'readonly [10, 20]'
const POINT = [10, 20] as const;

// Type '{ readonly text: "hello" }'
const PAYLOAD = { text: "hello" } as const;
複製代碼

所以,無需 TNAME[],咱們的 peoples 擁有了 TS 類型。
3d

image.png

同時,修改 getNameFromPeopleMap 的函數泛型聲明爲 <T extends typeof peoples[number]>(name: T): T。咱們便可約束函數傳入值只能爲 peoples 的子項。

可能有些同窗比較困惑,上述泛型是什麼意思。筆者這裏稍微解釋一下。

image.png

其實 typeof peoples[number] 真正能夠理解爲,先執行 typeof peoples,便可得出 readonly ["LIN", "HUA"] 類型。而後 [number] 能夠理解爲,從 readonly ["LIN", "HUA"] 中使用 number 類型做爲 key 將類型轉換爲字符串字面量類型 "LIN" | "HUA"。

那麼 T extends 即表明着進行泛型約束。所以咱們就能夠成功對函數傳參進行類型限制。

image.png

瞧一瞧,多麼成功。當我自信的把這段代碼發給朋友時,又遭到了無情吐槽,「我想要的是 peopleMap 直接使用」

的確,筆者反思了一下,爲何須要莫名其妙多一個函數去取值呢?還不是由於沒法從 convert 函數上完成傳參值和返回值的相等的類型限制嘛。

嗯,若是能從 convert 函數上直接支持就行了。

因而筆者將目光挪到了可疑的 convert 函數類型 (source: readonly K[]): Record<K, K> 上。首先從 Record<K, K> 上看,已經沒法約束類型了。按照需求,返回值應該是 key 與 value 相等的對象類型。怎麼寫呢?筆者查閱了 TS 文檔後,得出一種寫法:

type IRecord<K extends keyof any> = {
  [P in K]: P;
}
複製代碼

可能你們好奇 keyof any 是什麼?其實它剛恰好就是對象的 key 類型。由於 TS 限制對象 key 類型只能爲 string、number、symbol。

image.png

使用效果以下:

image.png

所以最終能夠幹掉以前多餘的函數,得出以下類型:

type IRecord<K extends keyof any> = {
  [P in K]: P;
}

function convert<K extends string>(source: readonly K[]): IRecord<K> {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}

const peoples = ['LIN', 'HUA'] as const;
const peopleMap = convert<typeof peoples[number]>(peoples);
const t = peopleMap.LIN;
複製代碼

同時也達到了對 peopleMap 的取值類型限制。

image.png


將代碼發給了朋友,終於獲得了滿意。可是他仍是但願能夠簡化一下。

好叭,那簡化一下!

function convert<K extends keyof any>(source: readonly K[]): { [P in K]: P }
{
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}

const peoples = ['LIN', 'HUA'] as const;
const peopleMap = convert(peoples);
const t = peopleMap.LIN;
複製代碼

簡簡單單的一個 TS 類型的需求,發現編寫起來仍是挺有意思的。第一,做爲筆記;第二,也但願能夠幫助到有須要的同窗們。

謝謝你們~若有助益,不勝榮幸!

相關文章
相關標籤/搜索