泛型简单来说可以理解成把类型当变量传到类型定义里,就如同参数传到函数一样,例如:
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 可能会更清晰。
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