泛型入门#
泛型簡單來說可以理解成把類型當變量傳到類型定義裡,就如同參數傳到函數一樣,例如:
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 來說 a
、b
、c
根本沒有和他的值的參數類型進行綁定,所以即使用了 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#30581 和 pull#47109