TypeScript 泛型解析

泛型入门

泛型简单来说可以理解成把类型当变量传到类型定义里，就如同参数传到函数一样，例如：

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

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

使用 <> 包裹 Type 就能把类型传入泛型函数，上面函数的效果是：接受类型为 Type 的参数，返回类型为 Type 的结果。

既然 Type 是个参数，那名字自然也是很自由的，我们常常会使用 T 当参数的名称，所以可以这么写：

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

自我推断

在使用泛型函数时可以不明确指出 T 的类型（而且我们通常都会这么做），此时 TS 将自动推断 T 的类型：

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

还是上面的例子，如果不显式指定类型 <string>，TS 就直接推断 "myString" 的类型为 T，所以这样函数返回的也是字符串。

画个圈

默认状态下泛型可以是任何类型，这样可读性就很低了，而且在对 “施加了泛型” 的类型进行操作或调用起方法时，因为是任意类型，必然不能通过检查，为了解决这个问题，可以通过 extends 给泛型画个圈，框定它的类型。

Copy
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 可以对同一个类型定义内运用到的其他泛型进行绑定。

再看一眼这个例子，其实他就是把输入的类型和输出的类型进行了绑定：

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

下面看一个 “类型绑定” 玩法更显眼的例子。

Copy
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

映射类型

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

假设有一个对象，它的 key 是 a、b、c，值是不同函数，现在我们需要得到一个 key 和对应函数参数的对象的类型，该如何实现呢？

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

如果这么写就坏了，Answer 只是一个 key 为 MyKey，value 为 Parameters<MyMap[MyKey]> 的对象，但是这两者间丢失了 myMap 定义的关系，变成这样：

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

所以这时候其实就要用到泛型对类型的绑定能力啦！正确答案如下：

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

K 是类型 myMap 的 key，并且，Answer2 的值必须是 MyMap[K] 的参数。这样就绑定了 Key 和值的固定关系。

甚至在新版本还有这种花里胡哨的，你可以把属性类型再 as 一次：

Copy
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>

输出结果如下：

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

P.S. as 其实是什么？官方文档称为 Type Assertions，用于把一个类型 as 为另一个类型，但是这两个类型，可以往小里 as，也可以往大去 as，但是必须有一方是另一方的子集。在 LazyPerson 的例子中因为说到底全是 string，所以可以使用 as。

实战

下面先放出题目，有兴趣可以先自己思考一下，如何完善下面 JS 函数的类型？（答案在下面，先别翻下去哦）

题目

首先我们有一个 myMap，但不直接使用它，而是先通过 wrapper 把它包装一下，这样就可以实现在运行函数时先做某些前置操作了，那么问题是，wrappedMap 的类型怎么写呢？

Copy
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])
}

答案

Copy
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

Copy
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

传送门：https://ssshooter.com/typescript-generics/