SSShooter

SSShooter

Write like you're running out of time.

TypeScript 泛型解析

泛型入门#

泛型簡單來說可以理解成把類型當變量傳到類型定義裡,就如同參數傳到函數一樣,例如:

function identity<Type>(arg: Type): Type {
  return arg
}

let output = identity<string>('myString')

使用 <> 包裹 Type 就能把類型傳入泛型函數,上面函數的效果是:接受類型為 Type 的參數,返回類型為 Type 的結果。

既然 Type 是個參數,那名字自然也是很自由的,我們常常會使用 T 當參數的名稱,所以可以這麼寫:

function identity<T>(arg: T): T {
  return arg
}

自我推斷#

在使用泛型函數時可以不明確指出 T 的類型(而且我們通常都會這麼做),此時 TS 將自動推斷 T 的類型:

let output = identity('myString')
// output 也是 string

還是上面的例子,如果不顯式指定類型 <string>,TS 就直接推斷 "myString" 的類型為 T,所以這樣函數返回的也是字符串。

画个圈#

默認狀態下泛型可以是任何類型,這樣可讀性就很低了,而且在對 “施加了泛型” 的類型進行操作或調用起方法時,因為是任意類型,必然不能通過檢查,為了解決這個問題,可以通過 extends 給泛型畫個圈,框定它的類型。

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // arg 必定有 length 屬性,通過類型檢查
  return arg
}

上面的代碼通過 <T extends Lengthwise> 表明這個 T 必須是一個有 length 屬性的類型,任何有 length 屬性的類型都滿足這個泛型的需求,例如,它也可以是一個數組。

绑定能力#

據官網所說泛型用於復用類型,相信經過上面的簡單介紹也會覺得這確實十分有效。但是泛型除了用於類型復用,還有什麼其他運用呢?

我的答案是類型的聯動,T 可以對同一個類型定義內運用到的其他泛型進行綁定

再看一眼這個例子,其實他就是把輸入的類型和輸出的類型進行了綁定:

function identity<Type>(arg: Type): Type {
  return arg
}

下面看一個 “類型綁定” 玩法更顯眼的例子。

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}

let x = { a: 1, b: 2, c: 3, d: 4 }

getProperty(x, 'a') // 可以
getProperty(x, 'm') // 報錯,因為 `Key` 綁定為 `Type` 的 key,而 m 並非 `Type` 的 key

映射類型#

const myMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}
type MyMap = typeof myMap
type MyKey = keyof MyMap

假設有一個對象,它的 key 是 a、b、c,值是不同函數,現在我們需要得到一個 key 和對應函數參數的對象的類型,該如何實現呢?

type Answer = Record<MyKey, Parameters<MyMap[MyKey]>>

如果這麼寫就壞了,Answer 只是一个 key 为 MyKey,value 为 Parameters<MyMap[MyKey]> 的對象,但是這兩者間丟失了 myMap 定義的關係,變成這樣:

type Answer = {
  a: [] | [someString: string] | [someNumber: number]
  b: [] | [someString: string] | [someNumber: number]
  c: [] | [someString: string] | [someNumber: number]
}

所以這時候其實就要用到泛型對類型的綁定能力啦!正確答案如下:

type Answer2 = {
  [K in MyKey]: Parameters<MyMap[K]>
}

K 是類型 myMap 的 key,並且,Answer2 的值必須是 MyMap[K] 的參數。這樣就綁定了 Key 和值的固定關係

甚至在新版本還有這種花里胡哨的,你可以把屬性類型再 as 一次:

type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<
    string & Property
  >}`]: () => Type[Property]
}

interface Person {
  name: string
  age: number
  location: string
}

type LazyPerson = Getters<Person>

輸出結果如下:

type LazyPerson = {
  getName: () => string
  getAge: () => number
  getLocation: () => string
}

P.S. as 其實是什麼?官方文檔稱為 Type Assertions,用於把一個類型 as 為另一個類型,但是這兩個類型,可以往小裡 as,也可以往大去 as,但是必須有一方是另一方的子集。在 LazyPerson 的例子中因為說到底全是 string,所以可以使用 as

實戰#

下面先放出題目,有興趣可以先自己思考一下,如何完善下面 JS 函數的類型?(答案在下面,先別翻下去哦)

題目#

首先我們有一個 myMap,但不直接使用它,而是先通過 wrapper 把它包裝一下,這樣就可以實現在運行函數時先做某些前置操作了,那麼問題是,wrappedMap 的類型怎麼寫呢?

const myMap = {
  a: () => {},
  b: (someString) => {},
  c: (someNumber) => {},
}

function wrapper(_key, fn) {
  return async function (...arg) {
    // do something
    await Promise.resolve()
    fn.apply(null, arg)
  }
}

const wrappedMap = {}
for (const key in myMap) {
  const k = key
  wrappedMap[k] = wrapper(k, myMap[k])
}

答案#

const myMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}
type MyMap = typeof myMap
type MyKey = keyof MyMap

function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
  return async function (...arg: Parameters<T>) {
    await Promise.resolve()
    ;(fn as any).apply(null, arg)
  }
}

type WrappedMap = {
  [K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
}

const wrappedMap: Partial<WrappedMap> = {}
for (const key in myMap) {
  const k = key as MyKey
  wrappedMap[k] = wrapper(k, myMap[k])
}

現在確實是已經做到了 WrappedMap 的類型是 wrapper 返回值的效果,但是,這句 (fn as any).apply(null, arg),是不是顯得很突兀?

為什麼還需要把 fn 置為 any?

因為對 TS 來說 abc 根本沒有和他的值的參數類型進行綁定,所以即使用了 T 進行限制也沒有效果,這句話可能有點拗口,接著看下面的答案 2 可能會更清晰。

答案 2#

const myMap: MyMap = {
  a: () => {},
  b: (someString: string) => {},
  c: (someNumber: number) => {},
}
interface MyMapArgs {
  a: []
  b: [someString: string]
  c: [someNumber: number]
}

type MyMap = {
  [K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void
}

type MyKey = keyof MyMap

function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
  return async function (...arg: Parameters<F>) {
    await Promise.resolve()
    fn.apply(null, arg)
  }
}

type WrappedMay = {
  [K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
}

const wrappedMap: Partial<WrappedMay> = {}
for (const key in myMap) {
  const k = key as MyKey
  wrappedMap[k] = wrapper(k, myMap[k])
}

去除 (fn as any) 的解法是,先另外造一個 map 把你需要關聯的東西先映射一遍,就是上面的 MyMapArgs,接著再用這個映射造出 MyMap,這樣 TS 才終於明白這兩個東西是有關係的。

P.S. 更詳細的信息可以參考 issues#30581pull#47109

傳送門:https://ssshooter.com/typescript-generics/

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。