玩转Typescript(二):TypeScript 泛型

748 阅读6分钟

这是我参与11月更文挑战的第 2 天,活动详情查看:2021最后一次更文挑战

在 C# 和 Java 等语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型之Hello World

假如我们想定义一个函数identity,传参arg的类型是string,返回值是arg本身。不用泛型的话,该函数如下所示:

function identity(arg: string): string {
  return arg;
}

为了使传参支持任意类型的,我们可以借助类型变量T,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
  return arg;
}

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。

我们定义了泛型函数后,可以用两种方法使用。

第一种方法,传入所有的参数,包含类型参数:

let output = identity<string>("Hello World");  // type of output will be 'Hello World'

第二种方法,利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型。类型推论帮助我们保持代码精简和高可读性。

let output = identity("Hello World");  // type of output will be 'Hello World'

函数传参可以是类型变量T组成的数组T[]。代码如下:

function loggingIdentity<T>(arg: T[]): T[] {
  console.log(arg.length);
  return arg;
}

我们也可以借助Array<T>实现上面的例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length);
  return arg;
}

泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数T在最前面,像函数声明一样:

function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
console.log(typeof myIdentity) // "function"

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
console.log(typeof myIdentity) // "function"

使用多个泛型参数:

function identity<T, L>(name: T, len: L): [T, L] {
  return [name, len];
}

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;

这引导我们去写第一个泛型接口了。

泛型接口

我们把上面例子里的对象字面量拿出来做为一个接口:

function identity<T>(arg: T): T {
  return arg;
}
interface GenericIdentityFn {
  <T>(arg: T): T;
}
let myIdentity: GenericIdentityFn = identity;

我们可能想把泛型参数T当作整个接口的一个参数,这样接口里的其它成员也能知道这个参数的类型了。

function identity<T>(arg: T): T {
  return arg;
}
interface GenericIdentityFn<T> {
  (arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;

泛型类

除了泛型接口,我们还可以创建泛型类。泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

类有两部分:静态部分和实例部分。泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

注意,无法创建泛型枚举和泛型命名空间。

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

GenericNumber泛型类没有被限制为只能使用number类型,也可以使用字符串或其它更复杂的类型。

泛型约束

确保属性存在

我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);  // Now we know it has a .length property, so no more error
  return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型。我们需要传入符合约束类型的值,必须包含必须的属性length

loggingIdentity(3);  // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3});

检查对象上的键是否存在

你可以声明一个类型参数,且它被另一个类型参数所约束。

比如,现在我们想要用属性名从对象里获取这个属性,并且我们想要确保这个属性存在于对象上

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

泛型类

在类中使用泛型,我们只需要在类名后面,添加多个类型变量<T, ...>

interface MyInt<U> {
  value: U
  getValue: () => U
}
class MyClass<T> implements MyInt<T> {
  value: T
  constructor(value: T) {
    this.value = value
  }
  getValue(): T {
    return this.value
  }
}
const myNumberClass = new MyClass<number>(111);
console.log(myNumberClass.getValue()); // 111
const myStringClass = new MyClass<string>("hhh");
console.log(myStringClass.getValue()); // hhh

使用泛型创建对象

构造签名

在 TypeScript 接口中,你可以使用 new 关键字来描述一个构造函数:

interface Point {
  new (x: number, y: number): Point;
}

以上接口中的 new (x: number, y: number)我们称之为构造签名,其语法如下:

ConstructSignature: newTypeParametersopt ( ParameterListopt ) TypeAnnotationopt
// TypeParametersopt 、ParameterListopt 和 TypeAnnotationopt 分别表示:可选的类型参数、可选的参数列表和可选的类型注解

构造函数类型

在 TypeScript 语言规范中,构造函数类型的定义是:

包含一个或多个构造签名的对象类型被称为构造函数类型;

构造函数类型可以使用构造函数类型字面量包含构造签名的对象类型字面量来编写。

那么什么是构造函数类型字面量呢?构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来说,构造函数类型字面量的形式如下:

new < T1, T2, ... > ( p1, p2, ... ) => R

该形式与以下对象类型字面量是等价的:

{ new < T1, T2, ... > ( p1, p2, ... ) : R }

实例1:

构造函数类型字面量:

// 构造函数类型字面量
new (x: number, y: number) => Point

等价于以下对象类型字面量:

// 对象类型字面量
{
  new (x: number, y: number): Point;
}

实例2:

interface Point {
  x: number;
  y: number;
}
interface PointConstructor { // 构造函数类型
  new (x: number, y: number): Point;
}
// 类实现接口
class Point2D implements Point {
  readonly x: number;
  readonly y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
// 定义函数 newPoint
function newPoint(pointConstructor: PointConstructor, x: number, y: number): Point {
  return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 2, 2);
console.log(point);

使用泛型创建对象

有时候,泛型类可能需要基于传入的泛型 T 来创建其类型相关的对象。比如:

class FirstClass {
  id?: number;
}
class SecondClass {
  name: string;
}
class GenericCreator<T> {
  create<T>(c: { new (): T }): T {
    return new c();
  }
}
const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create(FirstClass);
firstClass.id = 1;
const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create(SecondClass);
console.log(firstClass, secondClass)

在以上代码中,我们定义了两个普通类和一个泛型类 GenericCreator<T>。在通用的 GenericCreator 泛型类中,我们定义了一个名为 create 的成员方法,该方法会使用 new 关键字来调用传入的实际类型的构造函数,来创建对应的对象。

写完上面的代码,真觉得有时候Typescript写法太繁琐了... ==!

参考