TypeScript 泛型

156 阅读3分钟

简介

函数返回值的类型与参数类型是相关的。 泛型的特点就是带有“类型参数”(type parameter)。

function getFirst<T>(arr:T[]):T {
  return arr[0];
}

函数getFirst()的函数名后面尖括号的部分<T>,就是类型参数,类型声明需要的变量,需要在调用时传入具体的参数类型。

函数调用时,需要提供类型参数。

getFirst<number>([1, 2, 3])

多个类型参数

function map<T, U>(arr: T[], f: (arg: T) => U): U[] {
  return arr.map(f);
}

// 用法实例
map<string, number>(["1", "2", "3"], (n) => parseInt(n)); // 返回 [1, 2, 3]

泛型的写法

泛型主要用在四个场合:函数、接口、类和别名。

函数的泛型写法

function id<T>(arg: T): T {
  return arg;
}
// 写法一
let myId: <T>(arg: T) => T = id;

// 写法二
let myId: { <T>(arg: T): T } = id;

接口的泛型写法

使用泛型接口时,需要给出类型参数的值

interface Comparator<T> {
  compareTo(value:T): number;
}

class Rectangle implements Comparator<Rectangle> {

  compareTo(value:Rectangle): number {
    // ...
  }
}

它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。

interface Fn {
  <Type>(arg:Type): Type;
}

function id<Type>(arg:Type): Type {
  return arg;
}

let myId:Fn = id;

类的泛型写法

继承泛型类

class A<T> {
  value: T;
}

class B extends A<any> {
}

泛型也可以用在类表达式。

const Container = class<T> {
  constructor(private readonly data:T) {}
};

const a = new Container<boolean>(true);
const b = new Container<number>(0);
class C<NumType> {
  value!: NumType;
  add!: (x: NumType, y: NumType) => NumType;
}

let foo = new C<number>();

foo.value = 0;
foo.add = function (x, y) {
  return x + y;
};

泛型类写成构造函数

type MyClass<T> = new (...args: any[]) => T;

// 或者
interface MyClass<T> {
  new (...args: any[]): T;
}

// 用法实例
function createInstance<T>(AnyClass: MyClass<T>, ...args: any[]): T {
  return new AnyClass(...args);
}

泛型类描述的是类的实例,不包括静态属性和静态方法

class C<T> {
  static data: T;  // 报错
  constructor(public value:T) {}
}

类型别名的泛型写法

树形结构

type Tree<T> = {
  value: T;
  left: Tree<T> | null;
  right: Tree<T> | null;
};

类型参数的默认值

因为 TypeScript 会从实际参数推断出T的值,从而覆盖掉默认值

class Generic<T = string> {
  list:T[] = []

  add(t:T) {
    this.list.push(t)
  }
}
const g = new Generic();

g.add(4) // 报错
g.add('hello') // 正确

一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。

<T = boolean, U> // 错误

<T, U = boolean> // 正确

数组的泛型表示

number[]string[],只是Array<number>Array<string>的简写形式 Map<K, V>Set<T>Promise<T> ReadonlyArray<T>接口,表示只读数组

类型参数的约束条件

允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。

function comp<T extends { length: number }>(
  a: T,
  b: T
) {
  if (a.length >= b.length) {
    return a;
  }
  return b;
}

类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。

type Fn<A extends string, B extends string = "world"> = [A, B];

type Result = Fn<"hello">; // ["hello", "world"]

如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数。约束条件不能引用类型参数自身

<T, U extends T>
// 或者
<T extends U, U>

使用注意点

(1)尽量少用泛型。

泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。

(2)类型参数越少越好。

多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。

function filter<T, Fn extends (arg: T) => boolean>(arr: T[], func: Fn): T[] {
  return arr.filter(func);
}
function filter<T>(arr: T[], func: (arg: T) => boolean): T[] {
  return arr.filter(func);
}

(3)类型参数需要出现两次。

如果类型参数在定义后只出现一次,那么很可能是不必要的。

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
function greet(s: string) {
  console.log("Hello, " + s);
}

(4)泛型可以嵌套。

type OrNull<Type> = Type|null;

type OneOrMany<Type> = Type|Type[];

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;