关键词:typescript 泛型 generics 飞机 炸弹 斗地主
带着问题来做介绍
问题:
现在需要定义一个函数(过路飞机🛩),该函数接收一只 任意类型 的 飞机🛩 (plance)为参数,返回该 任意类型的类型 的 飞机🛩 (plance)。也就是说,返回的飞机类型和传入的飞机类型应该 一致 。
解释:
过路 => guo lu => Guo Lu => GL
飞机 => plance
过路飞机 => GLplance
炸弹 => bomb
要求分析:
- 传入 任意类型
- 返回的飞机的类型与传入的飞机的类型保持 一致性
作答
- 假设 任意类型 为
number,那我们可以这么写:
function GLPlance(plance: number): number {
return plance
}
// 业务方
GLPlance(444333)
- 假设 任意类型 为
string,那我们可以这么写:
function GLPlance(plance: string): string {
return plance
}
// 业务方
GLPlance('444333')
- 然而,(1)、(2)两种写法,都是传入 固定类型,并且我们不可能因为调用方传人参数类型的变更而去频繁修改函数,我们希望的是 任意类型 ,那我们使用
any来试试:
function GLPlance(plance: any): any {
return plance
}
// 业务方
GLPlance('444333')
- 很显然,如果是通过
any来处理这种情况的话,也是存在问题的,因为使用any就无法达到 一致性, 也就是说,传入了 飞机🛩 (plance),返回了 炸弹💣 (bomb):
function GLPlance(plance: any): any {
const bomb: number = 4444
return bomb
}
// 业务方
GLPlance('444333')
- 因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量 ,它是一种特殊的变量,只用于表示类型而不是值。
function GLPlance<P>(plance: P): P {
return plance
}
// 业务方
GLPlance<string>('444333')
作为编写 GLPlance 函数的我们,并不知道业务方(调用的那个人)调用我们的时候,会传入什么类型的变量,而我们需要定义与传入参数类型相同的 GLPlance 函数的返回类型。
也就是说,我们通过 <P> 定义了一个类型 P , 并且假设 GLPlance 函数的参数 plance 的类型为 P ,然后我们返回这个 P 类型的 plance 。
其实,就是相当于,我们需要在类型层面定义了一个变量 P ,然后,我们把这个变量 P 当作是一种类型,具体是什么类型呢,我们并不知道,只有业务方才知道(这就满足了 任意类型 的条件),然后我们再返回 P 类型的数据(传入参数类型与返回数据类型保持一致,满足了 一致性)。
泛型变量
上面所提到的通过 <> 创建出来的类型变量 P 就是 泛型变量,而且 泛型变量 很灵活。
问题:
我们把上面的问题稍微改变一下
现在需要定义一个函数(过路飞机🛩 ),该函数接收 任意类型 数组的 飞机队🛩 (plances)为参数,输出 飞机队🛩 (plances)的数量,然后返回该 任意类型的数组类型 的 飞机队🛩 (plances)。也就是说,返回的 飞机队🛩 (plances)的飞机类型和传入的 飞机队🛩 (plances)的飞机类型应该 一致 。
要求分析:
- 传入参数和返回数据为飞机类型的数组
作答:
定义 泛型变量 的方式不变,我们依旧是定义 P 作为飞机的类型,只是需要修改传入参数的定义和函数返回参数的定义,如下:
function GLPlances<P>(plances: P[]): P[] {
return plances
}
// 业务方
GLPlances<string>(['666555', '444333'])
泛型类型
问题:
如果我们要把该函数 GLPlance 赋值给一个变量,那这个变量的类型该怎么定义呢?
要求分析:
- 值为函数
GLPlance
作答:
- 先看一下正常类型的赋值语句:
function GLPlance(plance: number): number {
return plance
}
const myGLPlance: (plance: number) => number = GLPlance
该函数的类型就是 (plance: number) => number
- 其实,泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面:
function GLPlance<P>(plance: P): P {
return plance
}
const myGLPlance: <P>(plance: P) => P = GLPlance
该函数的类型就是 <P>(plance: P) => P,这种类型的写法,就像是写了一个 箭头函数
- 当然,在赋值语句中,我们也可以使用不同的泛型参数命名,只要在数量上和使用方式上对应好就可以
function GLPlance<P>(plance: P): P {
return plance
}
const myGLPlance: <T>(plance: T) => T = GLPlance
这里的 T 和 P 都是一个意思,都是指 plance 的类型。
GLPlance函数类型也可以写成这样{ <P>(plance: P): P }:
function GLPlance<P>(plance: P): P {
return plance
}
const myGLPlance: { <P>(plance: P): P } = GLPlance
- 当然,如果该类型
<P>(plance: P) => P使用的地方多了,总是这么写,代码的可维护性就低了,不太现实,我们可以把该类型定位为一个接口:
interface GenericGLPlanceFn {
<P>(plance: P) => P
}
function GLPlance<P>(plance: P): P {
return plance
}
const myGLPlance: GenericGLPlanceFn = GLPlance
// 业务方
myGLPlance('444333') // string
myGLPlance(444333) // number
- 另外,有时候,我们在赋值的时候需要先限制参数类型,也就是说,类型确定和函数调用分两步走:
interface GenericGLPlanceFn<P> {
(plance: P) => P
}
function GLPlance<P>(plance: P): P {
return plance
}
const myGLPlance: GenericGLPlanceFn<number> = GLPlance
// 业务方
myGLPlance(444333) // 只能使用number类型
泛型类
泛型类比较简单,与泛型接口类似,这里定义了一个 GLPlance 的类,并且提供了两个 实例属性 ,一个是 值 一个是 打 :
// 定义类
class GLPlance<T> {
value: T;
play: (plance: T, suffix: T) => T;
}
// 生成实例
const myGLPlance = new GLPlance<number>();
// 定义构造函数实例属性
myGLPlance.value = 333444;
myGLPlance.play = function(plance, suffix) {
return plance + suffix
}
myGLPlance.play(333444, 56) // 33344456
注意:
类的静态属性不能使用这个泛型。
class GLPlance<T>
static origin: T; // ❌
static origin2: string; // ✅
value: T; // ✅
play: (plance: T, suffix: T) => T; // ✅
}
泛型约束
问题:
众所周知,飞机🛩 是由 机身 (body)和 机尾 (suffix)组成,例如, 444333 带 5566 。那么我们希望在 GLPlance 执行过程中,顺便输出一下 body 和 suffix
要求分析:
- 飞机有 机身 (body)和 机尾 (suffix)
- 输出 机身 (body)和 机尾 (suffix)
作答:
- 我们先传入一只带有
body和suffix的飞机:
function GLPlance<P>(plance: P): P {
return plance
}
GLPlance({
body: 444333,
suffix: 5566
})
- 看起来没问题,那我们改一下函数,输出一下
body和suffix:
function GLPlance<P>(plance: P): P {
console.log('机身', plance.body) // Property 'body' does not exist on type 'P'.
console.log('机尾', plance.suffix) // Property 'suffix' does not exist on type 'P'.
return plance
}
GLPlance({
body: 444333,
suffix: 5566
})
编译器无法证明每种类型 P 都有 body 和 suffix 属性。
- 那么我们应该定义一个接口来描述约束条件,然后在 泛型变量 定义的时候,通过
extends关键字来实现 泛型约束
interface BasePlance {
body: number;
suffix: number;
}
function GLPlance<P extends BasePlance>(plance: P): P {
console.log('机身', plance.body) // 444333
console.log('机尾', plance.suffix) // 5566
return plance
}
GLPlance({
body: 444333,
suffix: 5566
})
- 然而现实生活中,我们在打飞机🛩的时候,是一个块一块打,比如我先打 机身 ,然后我再想想要带啥 机尾 :
function playPlance<Plance, Key extends keyof Plance>(plance: Plance, key: Key): Plance[Key] {
return plance[key]
}
// 先打机身
playPlance({
body: 444333,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'body') // 444333
// 想想要打那个做机尾
// 再打机尾
playPlance({
body: 444333,
suffix: 5566,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'suffix3') // 6699
泛型约束 - 在泛型约束中使用类型参数
问题:
现实生活中,我们在打飞机🛩的时候,是一个块一块打,比如我先打 机身 ,然后我再想想要带啥 机尾 ,那么要如何定义机尾的类型呢?
要求分析:
- 机尾 类型
- 先打 机身 ,再打 机尾
作答:
- 先定义好飞机的类型,然后试着打出 机身 和 机尾 :
function playPlance<Plance>(plance: Plance, suffixKey) {
return plance[suffixKey]
}
// 先打 机身
playPlance({
body: 444333,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'body') // 444333
// 想想要打那个做机尾
// 再打 机尾3
playPlance({
body: 444333,
suffix: 5566,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'suffix3') // 6699
// 随便打个 机尾4
playPlance({
body: 444333,
suffix: 5566,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'suffix4') // 不会报错,但是suffix4不存在于plance中,没有意义
- 我们希望,在打飞机的时候,输入
plance中不存在的属性key,语法上给出错误,那么suffixKey应该约束为机身中提供的属性,而不是随意输入:
function playPlance<Plance, Key extends keyof Plance>(plance: Plance, suffixKey: Key): Plance[Key] {
return plance[suffixKey]
}
// 先打机身
playPlance({
body: 444333,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'body') // 444333
// 想想要打那个做机尾
// 再打机尾
playPlance({
body: 444333,
suffix: 5566,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'suffix3') // 6699
// 随便打个 机尾4
playPlance({
body: 444333,
suffix: 5566,
suffix1: 5566,
suffix2: 7788,
suffix3: 6699,
}, 'suffix4') // Argument of type '"suffix4"' is not assignable to parameter of type '"body" | "suffix1" | "suffix2" | "suffix3" | "suffix"'.