TypeScript 学习【9】泛型

什么是泛型?

Java 语言中对泛型的解释是:泛型指的是类型参数化,即将原来某种具体的类型进行参数化。和定义函数参数一样,我们可以给泛型定义若干个类型参数,并在调用时给泛型传入明确的类型参数。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。

泛型类型参数

泛型最常用的场景是用来约束函数参数的类型,我们可以给函数定义若干个被调用时才会传入明确类型的参数。

比如以下定义的一个 reflect 函数 ,它可以接收一个任意类型的参数,并原封不动地返回参数的值和类型,那我们该如何描述这个函数呢?好像得用上 unknown 了

function reflect(param: unknown) {
  return param;
}
const str = reflect('string'); // str 类型是 unknown
const num = reflect(1); // num 类型 unknown

此时,reflect 函数虽然可以接收一个任意类型的参数并原封不动地返回参数的值,不过返回值类型不符合我们的预期。因为我们希望返回值类型与入参类型一一对应(比如 number 对 number、string 对 string),而不是无论入参是什么类型,返回值一律是 unknown。

此时,泛型正好可以满足这样的诉求,那如何定义一个泛型参数呢?首先,我们把参数 param 的类型定义为一个(类型层面的)参数、变量,而不是一个明确的类型,等到函数调用时再传入明确的类型。

比如我们可以通过尖括号 <> 语法给函数定义一个泛型参数 P,并指定 param 参数的类型为 P ,如下代码所示:

function reflect<P>(param: P) {
  return param;
}

这里我们可以看到,尖括号中的 P 表示泛型参数的定义,param 后的 P 表示参数的类型是泛型 P(即类型受 P 约束)。

我们也可以使用泛型显式地注解返回值的类型,虽然没有这个必要(因为返回值的类型可以基于上下文推断出来)。比如调用如下所示的 reflect 时,我们可以通过尖括号 <> 语法给泛型参数 P 显式地传入一个明确的类型。

function reflect<P>(param: P): P {
  return param;
}

然后在调用函数时,我们也通过 <> 语法指定了如下所示的 string、number 类型入参,相应地,reflectStr 的类型是 string,reflectNum 的类型是 number。

const str = reflect<string>('string'); // str 类型为 string
const num = reflect<number>(7); // num 类型为 number

泛型不仅可以约束函数整个参数的类型,还可以约束参数属性、成员的类型,比如参数的类型可以是数组、对象,如下示例:

function reflectArray<P>(param: P[]) {
  return param;
}

const reflectArr = reflectArray([1, '1']); //  reflectArr 类型为 (string | number)[]

这里我们约束了 param 的类型是数组,数组的元素类型是泛型入参。

注意:函数的泛型入参必须和参数/参数成员建立有效的约束关系才有实际意义。 比如在下面示例中,我们定义了一个仅约束返回值类型的泛型,它是没有任何意义的。

function uselessGeneries<P>(): P {
  return void 0 as unknown as P;
}

我们可以给函数定义任何个数的泛型入参,如下代码所示:

function reflectExtraParams<P, Q>(p1: P, p2: Q): [P, Q] {
  return [p1, [p2];
}

在上述代码中,我们定义了一个拥有两个泛型入参(P 和 Q)的函数 reflectExtraParams,并通过 P 和 Q 约束函数参数 p1、p2 和返回值的类型。

泛性类

在类的定义中,我们还可以使用泛型用来约束构造函数、属性、方法的类型,如下代码所示:

class Memory<S> {
  store: S;
  constructor(store: S) {
    this.store = store;
  }
  set(store: S) {
    this.store = store;
  }
  get() {
    return this.store;
  }
}

const numMemory = new  Memory(1);
const getNumMemory = numMemory.get(); // 类型是 number
numMemory.set(2);// 只能写入 number 类型
console.log(numMemory.get()); // 2
const strMemory = new Memory('');
const getStrMemory = strMemory.get(); // 类型是 string
strMemory.set('string'); // 只能写入 string 类型
console.log(strMemory.get()); // string  

首先,我们定义了一个支持读写的寄存器类 Memory,并使用泛型约束了 Memory 类的构造器函数、set 和 get 方法形参的类型,最后实例化了泛型入参分别是 number 和 string 类型的两种寄存器。

泛型类和泛型函数类似的地方在于,在创建类实例时,如果受泛型约束的参数传入了明确值,则泛型入参(确切地说是传入的类型)可缺省

泛型类型

在 TypeScript 中,类型本身就可以被定义为拥有不明确的类型参数的泛型,并且可以接收明确类型作为入参,从而衍生出更具体的类型,如下代码所示:

function reflect<P>(param: P):P {
  return param;
}

const reflectFn: <P>(param: P) => P = reflect; // 没问题

这里我们为变量 reflectFn 显式添加了泛型类型注解,并将 reflect 函数作为值赋给了它。

我们也可以把 reflectFn 的类型注解提取为一个能被复用的类型别名或者接口,如下代码所示:

function reflect<P>(param: P):P {
  return param;
}
type ReflectFunction = <P>(param: P) => P;
interface IReflcetFunction {
  <P>(param: P): P;
}

const reflectFun2: ReflectFunction = reflect;
const reflectFun3: IReflcetFunction = reflect;

将类型入参的定义移动到类型别名或接口名称后,此时定义的一个接收具体类型入参后返回一个新类型的类型就是泛型类型。

如下示例中,我们定义了两个可以接收入参 P 的泛型类型

function reflect<P>(param: P):P {
  return param;
}
type GenericReflectFunction<P> = (param: P) => P;
interface IGenericReflectFunction<P> {
  (param: P): P;
}
const reflectFun4: GenericReflectFunction<string> = reflect; // 具象化泛型
const reflectFun5: IGenericReflectFunction<number> = reflect; // 具象化泛型
const reflectFn4Return = reflectFun4('string'); // 入参和返回值都必须是 string 类型
const reflectFn5Return = reflectFun5(1); // 入参和返回值都必须是 number 类型

在泛型定义中,我们甚至可以使用一些类型操作符进行运算表达,使得泛型可以根据入参的类型衍生出各异的类型,如下代码所示:

type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type StringArray = StringOrNumberArray<string>; // 类型是 string[]
type NumberArray = StringOrNumberArray<number>; // 类型是 number[]
type NeverGot = StringOrNumberArray<boolean>; // 类型是 boolean

这里我们定义了一个泛型,如果入参是 number | string 就会生成一个数组类型,否则就生成入参类型。而且,我们还使用了与 JavaScript 三元表达式完全一致的语法来表达类型运算的逻辑关系。

如果我们给上面这个泛型传入了一个 string | boolean 联合类型作为入参,将会得到什么类型呢?

type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type BooleanOrString = string | boolean;

type WhatIsThis = StringOrNumberArray<BooleanOrString>; // 好像应该是 string | boolean ?
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; //  string | boolean

你会发现显示的类型将是 boolean | string[]

BooleanOrStringGot 和 WhatIsThis 这两个类型别名的类型居然不一样?这个就是所谓的分配条件类型

**关于分配条件类型这个概念,官方的释义:**在条件类型判断的情况下(比如上边示例中出现的 extends),如果入参是联合类型,则会被拆解成一个个独立的(原子)类型(成员)进行类型运算。

比如上边示例中的 string | boolean 入参,先被拆解成 string 和 boolean 这两个独立类型,再分别判断是否是 string | number 类型的子集。因为 string 是子集而 boolean 不是,所以最终我们得到的 WhatIsThis 的类型是 boolean | string[]

注意:枚举类型不支持泛型。

泛型约束

前面提到了泛型就像是类型的函数,它可以抽象、封装并接收(类型)入参,而泛型的入参也拥有类似函数入参的特性。因此,我们可以把泛型入参限定在一个相对更明确的集合内,以便对入参进行约束。

比如最前边提到的原封不动返回参数的 reflect 函数,我们希望把接收参数的类型限定在几种原始类型的集合中,此时就可以使用“泛型入参名 extends 类型”语法达到这个目的,如下代码所示:

function reflectSpecified<P extends number | string | boolean>(param: P): P {
  return param;
}

reflectSpecified('string');
reflectSpecified(7);
reflectSpecified(true);
reflectSpecified(null); // ts(2345) 'null' 不能赋予类型 'number | string | boolean'

同样,我们也可以把接口泛型入参约束在特定的范围内,如下代码所示:

interface ReduxModelSpecified<State extends { id: number; name: string }> {
  state: State;
}

type ComputedReduxModel1 = ReduxModelSpecified<{ id: number; name: string }>;
type ComputedReduxModel2 = ReduxModelSpecified<{ id: number; name: string; age: number }>;
type ComputedReduxModel3 = ReduxModelSpecified<{ id: string; name: number }>; // ts(2344)
type ComputedReduxModel4 = ReduxModelSpecified<{ id: number }>; // ts(2344)

在上述示例中,ReduxModelSpecified 泛型仅接收 { id: number; name: string } 接口类型的子类型作为入参。

我们还可以在多个不同的泛型入参之间设置约束关系,如下代码所示:

interface ObjSetter {
  <O extends {}, K extends keyof O, V extends O[K]>(obj: O, key: K, value: V): V;
}

const setValueOfObj: ObjSetter = (obj, key, value) => (obj[key] = value);
setValueOfObj({ id: 1, name: 'name' }, 'id', 2);
setValueOfObj({ id: 1, name: 'name' }, 'name', 'newName');
setValueOfObj({ id: 1, name: 'name' }, 'age', 2);
setValueOfObj({ id: 1, name: 'name' }, 'id', '2');

在设置对象属性值的函数类型时,它拥有 3 个泛型入参:第 1 个是对象,第 2 个是第 1 个入参属性名集合的子集,第 3 个是指定属性类型的子类型。

另外,泛型入参与函数入参还有一个相似的地方在于,它也可以给泛型入参指定默认值(默认类型),且语法和指定函数默认参数完全一致,如下代码所示:

interface ReduxModelSpecified<State extends { id: number; name: string }> {
  state: State
}
interface ReduxModelSpecified2<State = { id: number; name: string }> {
  state: State
}
type ComputedReduxModel5 = ReduxModelSpecified2;
type ComputedReduxModel6 = ReduxModelSpecified2<{ id: number; name: string; }>;
type ComputedReduxModel7 = ReduxModelSpecified; // ts(2314) 缺少一个类型参数

在上述示例中,我们定义了入参有默认类型的泛型 ReduxModelSpecified2,因此使用 ReduxModelSpecified2 时类型入参可缺省。而 ReduxModelSpecified 的入参没有默认值,所以缺省入参时会提示一个类型错误。

泛型入参的约束与默认值还可以组合使用,如下代码所示:

interface ReduxModelMixed<State extends {} = { id: number; name: string }> {
  state: State
}

这里我们限定了泛型 ReduxModelMixed 入参 State 必须是 {} 类型的子类型,同时也指定了入参缺省时的默认类型是接口类型 { id: number; name: string; }。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大杯美式不加糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值