文章

TS泛型

TS泛型

泛型

泛型写法

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

函数上的泛型

function 关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。

1
2
3
function id<T>(arg:T):T {
  return arg;
}

于变量形式定义的函数,泛型有下面两种写法:

1
2
3
4
5
// 写法一
let myId:<T>(arg:T) => T = id;

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

接口的泛型

1
2
3
4
5
interface Box<Type> {
  contents: Type;
}

let box:Box<string>;

泛型接口还有第二种写法。

1
2
3
4
5
6
7
8
9
interface Fn {
  <Type>(arg:Type): Type;
}

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

let myId:Fn = id;

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

类的泛型

泛型类的类型参数写在类名后面

1
2
3
4
class Pair<K, V> {
  key: K;
  value: V;
}

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

1
2
3
4
5
6
const Container = class<T> {
  constructor(private readonly data:T) {}
};

const a = new Container<boolean>(true);
const b = new Container<number>(0);

JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

类型别名的泛型写法

type 命令定义的类型别名,也可以使用泛型。

1
type Nullable<T> = T | undefined | null;

Nullable<T> 是一个泛型,只要传入一个类型,就可以得到这个类型与 undefined 和 null 的一个联合类型

1
2
3
4
5
6
7
8
9
10
11
type Container<T> = { value: T };

const a: Container<number> = { value: 0 };
const b: Container<string> = { value: 'b' };

// 定义树形结构的例子
type Tree<T> = {
  value: T;
  left: Tree<T> | null;
  right: Tree<T> | null;
};

类型参数的默认值

类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。

1
2
3
4
5
function getFirst<T = string>(
  arr:T[]
):T {
  return arr[0];
}

类型参数的默认值,往往用在类中。

1
2
3
4
5
6
7
class Generic<T = string> {
  list:T[] = []

  add(t:T) {
    this.list.push(t)
  }
}

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

1
2
3
<T = boolean, U> // 错误

<T, U = boolean> // 正确

数组的泛型表示

数组类型有一种表示方法是 Array<T>。这就是泛型的写法,Array 是 TypeScript 原生的一个类型接口,T 是它的类型参数。声明数组时,需要提供 T 的值。

1
let arr:Array<number> = [1, 2, 3];

TypeScript 默认还提供一个 ReadonlyArray<T> 接口,表示只读数组。

1
2
3
4
5
function doStuff(
  values:ReadonlyArray<string>
) {
  values.push('hello!');  // 报错
}

参数 values 的类型是 ReadonlyArray<string>,表示不能修改这个数组,所以函数体内部新增数组成员就会报错。因此,如果不希望函数内部改动参数数组,就可以将该参数数组声明为 ReadonlyArray<T> 类型。

类型参数的约束条件

TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
function comp<T extends { length: number }>(
  a: T,
  b: T
) {
  if (a.length >= b.length) {
    return a;
  }
  return b;
}

comp([1, 2], [1, 2, 3]) // 正确
comp('ab', 'abc') // 正确
comp(1, 2) // 报错

T extends { length: number } 就是约束条件,表示类型参数 T 必须满足 { length: number },否则就会报错。

类型参数的约束条件采用下面的形式:,TypeParameter 表示类型参数,extends 是关键字,这是必须的,ConstraintType 表示类型参数要满足的条件,即类型参数应该是 ConstraintType 的子类型。

1
<TypeParameter extends ConstraintType>

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

1
2
3
4
type Fn<A extends string, B extends string = 'world'>
  =  [A, B];

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

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

1
2
3
<T, U extends T>
// 或者
<T extends U, U>

但是,约束条件不能引用类型参数自身。

1
2
<T extends T>               // 报错
<T extends U, U extends T>  // 报错

Ref

https://wangdoc.com/typescript/generics

本文由作者按照 CC BY 4.0 进行授权