仓库源文站点原文


title: TypeScript 之泛型 layout: post categories: JavaScript tags: ts typescript 泛型 generic

excerpt: 介绍TypeScript中泛型的概念与使用场景

背景

泛型用于创建可复用的支持多种类型的组件,比如不仅能支持当前的类型,还能支持未来的类型,为大型系统的构建提供一定灵活性,有广泛、多种的意思,即泛型可实现对多种类型的支持;泛型是一种已有的概念,除了 TypeScript,同样也存在于其他多种语言中;

先举一个基本的例子,ts 中实现一个加法运算的函数,可以是这样的:

function addFn(arg1: number, arg2: number): number {
    return arg1 + arg2;
}
addFn(1, 2);

如果后期功能拓展,需要上述函数也具备拼接字符串的功能,即:

function addFn(arg1: string, arg2: string): string {
    return arg1 + arg2;
}
addFn('a', 'b');

但是这样的申明并不与上面已有的申明兼容,即使使用联合类型处理也比较复杂,但是它们的处理逻辑却是一样的,只是类型值换了,重写一个新函数也不符合拓展的初衷,所以需要寻求其他方法;

函数重载

在 ts 中重载是为一个函数提供多个类型定义的操作,使得函数可以根据传参的不同而拥有不同的返回类型;这样,我们就能轻松实现之前例子的拓展需求:

function addFn(arg1: string, arg2: string): string;
function addFn(arg1: number, arg2: number): number;
function addFn(arg1: any, arg2: any): any {
  // 上面一行只是函数的实现签名,为了兼容上面两个重载签名,不能被直接调用,
  // 同时它也并不算作一个重载,真正的重载签名只有最上面的两个
  return arg1 + arg2;
}

// 以下代码都能通过类型检查
addFn(1, 2);
addFn('a', 'b');

// 而没在重载定义中的类型会报错
// addFn(true, true); // Error

这个例子中,该申明函数与正常函数的区别是:

使用重载虽然实现了同时能计算数字和拼接字符串的需求,但是这种写法还是有些复杂,因为参数类型与返回类型具有一定规律性;因此还可以继续寻求更简便的方式;

类型变量

在 ts 中,可以使用泛型来解决上述需求,具体的方式是使用类型变量,顾名思义,ts 包含一个庞大的类型处理系统,有各种使用类型的情景,为了应对一些场景,就需要类型值有像变量一样的变化性,支持赋值与取值操作;

先看一下使用类型变量的具体写法:

function addFn<T>(arg1: T, arg2: T): T {
    return arg1 + arg2;
}
addFn<number>(1, 2);
addFn<string>('1', '2');
addFn('a', 'b'); // 调用时也可以省略类型赋值

这里的写法就比写重载的形式简便了许多;示例中出现了 <T> 这个标识,其中 T 表示类型变量<> 表示对类型变量的申明,即申明时使用 <> 设置变量,调用时再使用 <> 进行赋值,这样所有用到变量 T 的地方都会被替换为传入的类型值;这里可以发现,泛型就像类型系统中一个针对类型的函数,类型参数就是形参

调用时可以省略类型赋值的操作是因为上面的场景中 ts 可以利用类型推断机制自动判断出 T 的实际类型值(number);

由于 T 表示任意类型,所以不能直接访问某些属性或方法:

function fn<T>(arg: T): T {
  // return arg.toString(); // Error,因为并不是所有类型都有该方法
  return arg;
}

如果是复合类型,则可以使用某些固有属性:

function fn<T>(arg: T[]): string {
  return arg.toString(); // 普通数组类型都具有该方法
}

类型变量也可以使用其他字母或者单词(通常使用 T),并且可以同时定义多个变量:

function fn<M, My, other>(arg: M): M {
  let one: My;
  let two: other;

  return arg;
}

// 存在多个类型变量时,需要依次赋值类型
fn<string, number, boolean>('abc');

泛型接口

除了函数,泛型也可以在接口中使用,例如:

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

let fn: IGeneric = function(arg) {
  return arg;

  // 和接口申明不一致会报错
  // return arg + '';
}

或者是针对整个接口的泛型:

interface IGeneric<T> {
  a: T;
  b: T[];
  c(arg: T): T;
}

const obj: IGeneric<number> = {
  a: 123,
  b: [1, 2, 3],
  c: (arg) => arg + 1
};

泛型类

针对类定义类型变量时,类的所有非静态成员都可以使用该变量:

class CS<T> {
  constructor(public attr: T) {}

  fn(): T {
    return this.attr;
  }

  // 静态成员不能使用泛型类型
  // static a: T = ''; // Error
}

// 实例化时传入类型值
const cs = new CS<number>(123);

cs.fn(); // 123

泛型约束

在一些场景中,可能并不期望类型变量的值太,而是需要具有某些属性或方法,这时就可以使用 extends 关键字对类型加一些约束,这和类中的继承用途也有些类似;例如:

interface IObj {
  length: number;
}

// 将参数约束为具有 length 属性的任意类型
function fn<T extends IObj>(arg: T): number {
  return arg.length;
}

fn('abc'); // 3

// 报错,因为数字没有 length 属性
// fn(123); // Error

结合其他 ts 特性,也能表示一些特殊情形,比如下例表示函数第二个参数,需要是第一个参数对象的属性名之一,keyof 关键字为索引查询操作符,keyof T 表示 T 的所有属性构成的联合类型:

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

fn({ a: 1 }, 'a');

// 报错,因为第一个参数对象中并没有一个叫 'b' 的属性
// fn({ a: 1 }, 'b'); // Error