带着问题学泛型 TypeScript Generics

624 阅读7分钟

关键词:typescript 泛型 generics 飞机 炸弹 斗地主

带着问题来做介绍

问题:

现在需要定义一个函数(过路飞机🛩),该函数接收一只 任意类型飞机🛩 (plance)为参数,返回该 任意类型的类型飞机🛩 (plance)。也就是说,返回的飞机类型和传入的飞机类型应该 一致

解释:

过路 => guo lu => Guo Lu => GL

飞机 => plance

过路飞机 => GLplance

炸弹 => bomb

要求分析:

  1. 传入 任意类型
  2. 返回的飞机的类型与传入的飞机的类型保持 一致性

作答

  1. 假设 任意类型number ,那我们可以这么写:
function GLPlance(plance: number): number {
	return plance
}

// 业务方
GLPlance(444333)
  1. 假设 任意类型string ,那我们可以这么写:
function GLPlance(plance: string): string {
	return plance
}

// 业务方
GLPlance('444333')
  1. 然而,(1)、(2)两种写法,都是传入 固定类型,并且我们不可能因为调用方传人参数类型的变更而去频繁修改函数,我们希望的是 任意类型 ,那我们使用 any 来试试:
function GLPlance(plance: any): any {
	return plance
}

// 业务方
GLPlance('444333')
  1. 很显然,如果是通过 any 来处理这种情况的话,也是存在问题的,因为使用 any 就无法达到 一致性, 也就是说,传入了 飞机🛩 (plance),返回了 炸弹💣 (bomb):

function GLPlance(plance: any): any {
	const bomb: number = 4444

	return bomb
}

// 业务方
GLPlance('444333')
  1. 因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量 ,它是一种特殊的变量,只用于表示类型而不是值。
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)的飞机类型应该 一致

要求分析:

  1. 传入参数和返回数据为飞机类型的数组

作答:

定义 泛型变量 的方式不变,我们依旧是定义 P 作为飞机的类型,只是需要修改传入参数的定义和函数返回参数的定义,如下:

function GLPlances<P>(plances: P[]): P[] {
	return plances
}

// 业务方
GLPlances<string>(['666555', '444333'])

泛型类型

问题:

如果我们要把该函数 GLPlance 赋值给一个变量,那这个变量的类型该怎么定义呢?

要求分析:

  1. 值为函数 GLPlance

作答:

  1. 先看一下正常类型的赋值语句:
function GLPlance(plance: number): number {
	return plance
}


const myGLPlance: (plance: number) => number = GLPlance

该函数的类型就是 (plance: number) => number

  1. 其实,泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面:
function GLPlance<P>(plance: P): P {
	return plance
}

const myGLPlance: <P>(plance: P) => P = GLPlance

该函数的类型就是 <P>(plance: P) => P,这种类型的写法,就像是写了一个 箭头函数

  1. 当然,在赋值语句中,我们也可以使用不同的泛型参数命名,只要在数量上和使用方式上对应好就可以
function GLPlance<P>(plance: P): P {
	return plance
}

const myGLPlance: <T>(plance: T) => T = GLPlance

这里的 TP 都是一个意思,都是指 plance 的类型。

  1. GLPlance 函数类型也可以写成这样 { <P>(plance: P): P }
function GLPlance<P>(plance: P): P {
	return plance
}

const myGLPlance: { <P>(plance: P): P } = GLPlance
  1. 当然,如果该类型 <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
  1. 另外,有时候,我们在赋值的时候需要先限制参数类型,也就是说,类型确定和函数调用分两步走:
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)组成,例如, 4443335566 。那么我们希望在 GLPlance 执行过程中,顺便输出一下 bodysuffix

要求分析:

  1. 飞机有 机身 (body)和 机尾 (suffix)
  2. 输出 机身 (body)和 机尾 (suffix)

作答:

  1. 我们先传入一只带有 bodysuffix 的飞机:
function GLPlance<P>(plance: P): P {
	return plance
}


GLPlance({
	body: 444333,
  suffix: 5566
})
  1. 看起来没问题,那我们改一下函数,输出一下 bodysuffix
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 都有 bodysuffix 属性。

  1. 那么我们应该定义一个接口来描述约束条件,然后在 泛型变量 定义的时候,通过 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
})
  1. 然而现实生活中,我们在打飞机🛩的时候,是一个块一块打,比如我先打 机身 ,然后我再想想要带啥 机尾 :
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

泛型约束 - 在泛型约束中使用类型参数

问题:

现实生活中,我们在打飞机🛩的时候,是一个块一块打,比如我先打 机身 ,然后我再想想要带啥 机尾 ,那么要如何定义机尾的类型呢?

要求分析:

  1. 机尾 类型
  2. 先打 机身 ,再打 机尾

作答:

  1. 先定义好飞机的类型,然后试着打出 机身机尾
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中,没有意义
  1. 我们希望,在打飞机的时候,输入 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"'.

参考

TypeScript Generics

TypeScript Generics 中文