typescript(5)- 泛型详解 | 青训营笔记

125 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第12天

「前言」

相较于 js,由于类型的存在,ts 中相似的逻辑因为类型的不同,使得代码复用性降低,针对于这一现象,ts 使用 泛型类型 使得相同的一段代码可以支持多种数据类型

「泛型类型」

泛型类型可以作为一种变量类型

function Test<T, K>(arg1: T, arg2: K): void { 
  let arg3: T;
}

Test<number, string>(1, 'hello');
  • 在这个例子中我们在 <> 中定义了两种暂时还不知道的类型,我们可以在函数内部使用这种类型变量,例如给参数添加类型注解,给返回值添加类型,甚至还可以在内部声明具有泛型类型的变量。
  • 我们在调用函数的时候可以指定泛型类型,从而使泛型类型具有更具体的类型

我们还可以使用类型推断简化调用的步骤

function Test<T, K>(arg1: T, arg2: K): void { 
  let arg3: T;
}

Test(1, 'hello');

还可以使用泛型作为全新的类型添加给变量

const test: <T>(arg: T) => T = (arg) => arg
const test: { <T>(arg: T): T } = (arg) => arg

可以使用 {} 包裹类型声明,将类型变得一体化

但是在定义泛型的时候,具有泛型类型的数据缺少了更细致的类型判断,因此丧失了诸多能力

function Test<T>(num1: T, num2: T) {
  num1 + num2; // error,运算符“+”不能应用于类型“T”和“T”
  num1.length; // error,类型“T”上不存在属性“length”。
}

后续会讲解各种解决方案

「泛型接口」

泛型除了可以运用在函数中,还可以在接口中使用

泛型接口声明函数

interface IFn {
  <T>(arg: T): T;
}

const fn: IFn = (arg) => arg;

接口使用泛型参数

在接口中还可以将泛型作为参数传递给接口

使用泛型参数重写上面的例子

interface IFn<T> {
  (arg: T): T;
}

const fn: IFn<number> = (arg) => arg;

在使用泛型参数声明函数的时候,必须传递给接口一个确切的类型,使得接口可以获得一个明确的类型

「泛型类」

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

class Test<T> {
  id: T;
  show: (arg: T) => T;
}

new Test()
new Test<number>();

因此,泛型类可以创建多种不同类型的实例

注意:静态成员不能使用泛型类的泛型类型

「泛型约束」

虽然泛型可以复用代码,但是使用泛型类型的变量缺少了诸多操作方法

function getStrLen<T>(arg: T): number {
  return arg.length; // error,类型“T”上不存在属性“length”
}

由于 <T> 这个泛型类型上缺少了 .length 属性,所以我们得想办法给 <T> 添加上 .length 属性,我们可以使用 泛型约束 给泛型添加更多的约束条件,使泛型类型具有确切的类型,从而拥有对应的属性

interface IT {
  length: number;
}


function getStrLen<T extends IT>(arg: T): number {
  return arg.length;
}

创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束

一个更具体的例子:

interface IT {
  length: number;
  method: () => void;
}


function getStrLen<T extends IT>(arg: T): number {
  arg.method();
  return arg.length;
}

interface IObj {
  length: number;
  method: () => void;
}

getStrLen<IObj>({
  length: 3,
  method() { }
});

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

很多时候,多个泛型参数之间存在依赖关系

比如,现在我们想要用属性名从对象里获取这个属性。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m"); // error,类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数。

这里的 keyof 关键字表示 <K><T> 中的一种索引
我们使用 extends 约束泛型 KK 必须是 T 中的键名

「在泛型里使用类类型」

ts 中 类名也可以作为参数传递

class Test { }

function createInstance(className: typeof Test): Test {
  return new className();
}

createInstance(Test);

在这个例子中我们使用 typeof Test 约束 className 的类型,这个函数等效于 new Test()

我们还可以使用其他的方式传递类名

class Test { }

function createInstance(className: { new(): Test }): Test {
  return new className();
}

createInstance(Test);

下面的方式与之前的两种等效

function createInstance(className: new () => Test): Test {
  return new className();
}

需求:使用上述方式构建出一个函数可以创建出任意实例

这个需求,相当上面的例子,复用性大大提升,这里肯定是使用泛型类型约束函数参数

function createInstance<T>(className: new () => T): T {
  return new className();
}

注意:这里不能使用 typeof 关键字获取泛型的类型,因为 typeof 关键字只能判断一个明确值的类型,不能判断类型(泛型类型也是一种类型)

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

class Animal {
  numLegs: number;
}

class Lion extends Animal { }

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion);

class Test { numLegs: number; }

createInstance(Test)

泛型 A 类型被 Animal 类型约束,所以必须传递符合约束规则的类

「泛型与unknown的联系」

function add(num: unknown) {
  return num + 0; // error,运算符“+”不能应用于类型“unknown”和“0”。
}

function add1<T>(num: T) {
  return num + 0; // error,运算符“+”不能应用于类型“T”和“number”。
}

通过这个例子可以看出 泛型unknown 十分相似

泛型的解决办法

  1. 不能使用类型断言
function test<T>(arg: T) {
  return (arg as string) + 0; // error,类型 "T" 到类型 "string" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"
}
  1. 只能使用更细致的类型判断
function test<T>(arg: T) {
  if (typeof arg === 'number') {
    return arg + 0;
  }
}
  1. 如果想要获取泛型类型变量的属性,还可以使用类型约束

unknown 的解决办法

  1. 可以使用类型断言
  2. 也可以使用更细致的类型判断

「参考文章」

泛型的使用绝不止于这篇文章的介绍,关于泛型的高级使用可以参考:你不知道的 TypeScript 泛型(万字长文,建议收藏) - 知乎 (zhihu.com)