TypeScript Tutorial 中文版 - Section 8. 泛型

451 阅读9分钟

Section 8. 泛型

泛型

原文地址

在本教程中,你将学习 TypeScript 中的泛型,它允许把类型作为形参来使用。

TypeScript 中的泛型介绍

TypeScript 中的泛型可以编写可重用的泛型 函数,泛型 和 泛型 接口,在本教程中,你将专注于开发通用函数。

下面通过一个简单的例子解释 TypeScript 中的泛型的概念。

假设需要开发一个函数,它随机返回一个 数字 数组 中的元素。

下面的 getRandomNumberElement() 函数接受一个数字数组作为参数,并随机返回数组中的一个元素:

function getRandomNumberElement(items: number[]): number {
  let randomIndex = Math.floor(Math.random() * items.length);
  return items[randomIndex];
}

为了随机获取数组中的一个元素,你需要:

  • 先找到随机索引;
  • 根据随机索引获取随机元素。

要找到数组的随机索引,使用 Math.random() 函数,它返回 01 之间的随机数,将得到的随机数和数组的长度相乘,得到的结果传递给 Math.floor() 函数,得到随机索引。

下面演示如何使用 getRandomNumberElement() 函数:

let numbers = [1, 5, 7, 4, 2, 9];
console.log(getRandomNumberElement(numbers));

假设你需要从一个 字符串 数组中随机获取一个元素,你可能想到开发一个新的函数:

function getRandomStringElement(items: string[]): string {
  let randomIndex = Math.floor(Math.random() * items.length);
  return items[randomIndex];
}

getRandomStringElement() 函数的逻辑和 getRandomNumberElement() 函数的逻辑相同。

下面演示如何使用 getRandomStringElement() 函数:

let colors = ['red', 'green', 'blue'];
console.log(getRandomStringElement(colors));

稍后你可能需要随机获取对象数组中的一个元素,每次想从新的类型数组中随机获得一个元素的时候,都需要创建一个新的函数,这种方式不具有可扩展性。

使用 any 类型

这个问题的一个解决方案是把数组参数的类型设置为 any[] 类型,这样处理后只需要编写一个用于任何类型的数组的函数:

function getRandomAnyElement(items: any[]): any {
  let randomIndex = Math.floor(Math.random() * items.length);
  return items[randomIndex];
}

getRandomAnyElement() 函数适用于 any 类型的数组,包括数字,字符串和对象类型等等。

let numbers = [1, 5, 7, 4, 2, 9];
let colors = ['red', 'green', 'blue'];

console.log(getRandomAnyElement(numbers));
console.log(getRandomAnyElement(colors));

这个解决方法是有效的,但是它有一个缺点:无法强制指定返回元素的类型,换句话说,它不是类型安全的。想要在保留类型的同时避免重复的代码,使用泛型是一个很好的解决方案。

TypeScript 中的泛型可以帮上忙

下面是一个泛型函数,它从类型为 T 的数组中随机返回一个元素:

function getRandomElement<T>(items: T[]): T {
  let randomIndex = Math.floor(Math.random() * items.length);
  return items[randomIndex];
}

这个函数使用类型变量 TT 允许你捕获调用函数的时候提供的类型,此外,该函数使用 T 类型作为函数的返回类型。getRandomElement() 函数是通用的,因为它可以处理任何数据类型的数组,包括字符串,数字和对象类型等等。

按照惯例,我们使用 T 作为类型变量,当然你也可以使用其他字母,比如 ABC 等等。

调用泛型函数

下面演示如何使用数字数组来调用 getRandomElement() 函数:

let numbers = [1, 5, 7, 4, 2, 9];
let randomEle = getRandomElement<number>(numbers);
console.log(randomEle);

这个例子显式地把 T 类型对象赋值为 number,传递给 getRandomElement() 函数。

实践中,你可以使用 类型推断 来推断 T 类型对象的类型。这意味着你可以让 TypeScript 编译器根据你传递的参数自动设置 T 的值,如下所示:

let numbers = [1, 5, 7, 4, 2, 9];
let randomEle = getRandomElement(numbers);
console.log(randomEle);

在这个例子中,我们没有显式地给 getRandomElement() 函数传递 数字类型,编译器会把 T 设置为对应的类型。现在 getRandomElement() 函数也是类型安全的了,如果把返回值赋值给一个字符串变量,将会得到一个错误提示:

let numbers = [1, 5, 7, 4, 2, 9];
let returnElem: string;
returnElem = getRandomElement(numbers); // compiler error

具有多个类型变量的泛型函数

下面演示如何使用两个类型变量 UV 来开发泛型函数:

function merge<U, V>(obj1: U, obj2: V) {
  return {
    ...obj1,
    ...obj2,
  };
}

merge() 函数合并类型为 UV 的两个对象,将它们的属性组合成一个新的对象。 merge() 函数的返回类型被推断为 UV 的交集类型,即 U & V

下面演示了如何使用 merge() 函数来合并两个对象:

let result = merge({ name: 'John' }, { jobTitle: 'Frontend Developer' });

console.log(result);

输出:

{ name: 'John', jobTitle: 'Frontend Developer' }

TypeScript 中泛型的作用

下面是 TypeScript 中泛型的作用:

  • 编译时进行类型检查;
  • 消除 类型转换
  • 实现泛型算法。

小结

  • 使用 TypeScript 泛型可以开发可复用的,具有通用性的和类型安全的函数,接口或者类。

泛型约束

原文地址

在本教程中,你将学习 TypeScript 中的泛型约束。

TypeScript 中的泛型约束介绍

思考以下例子:

function merge<U, V>(obj1: U, obj2: V) {
  return {
    ...obj1,
    ...obj2,
  };
}

merge() 是一个可以合并两个对象的泛型函数:

let person = merge({ name: 'John' }, { age: 25 });

console.log(result);

输出:

{ name: 'John', age: 25 }

它工作地很好。merge() 函数接受两个对象,但它无法阻止你传递一个非对象的参数,如下所示:

let person = merge({ name: 'John' }, 25);

console.log(person);

输出:

{
  name: 'John';
}

TypeScript 没有发出任何的错误提示。你可能想给 merge() 函数添加一个约束,使得它只能处理对象而不处理其他的类型的参数。要做到这一点,你需要提出要求,作为 UV 类型的约束。

为了表示约束,你可以使用 extends 关键字:

function merge<U extends object, V extends object>(obj1: U, obj2: V) {
  return {
    ...obj1,
    ...obj2,
  };
}

因为 merge() 函数的参数受到了约束,它将不再适合用于所有类型的参数调用,它现在只适合用于 object 类型的参数调用。

下面将抛出一个错误提示:

let person = merge({ name: 'John' }, 25);

错误提示:

Argument of type '25' is not assignable to parameter of type 'object'.

在泛型约束中使用行参

TypeScript 允许声明受另外一个类型参数约束的类型参数。

下面的 prop() 函数接受一个对象和一个属性名,它会返回属性的值:

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

编译器会抛出下面的错误提示:

Type 'K' cannot be used to index type 'T'.

为了修复这个错误,你在 K 上添加一个约束来确保它是 T 类型的键,如下所示:

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

如果你传递给 prop() 函数一个 obj 对象上存在的属性明,编译器不会抛出错误提示,如下所示:

let str = prop({ name: 'John' }, 'name');
console.log(str);

输出:

John

然而如果传递一个在第一个参数上不存在的属性名,编译器会抛出一个错误提示:

let str = prop({ name: 'John' }, 'age');

错误提示:

Argument of type '"age"' is not assignable to parameter of type '"name"'.

小结

  • 使用 extends 关键字将类型参数约束为指定类型;
  • 使用 extends keyof 约束类型为另外一个对象的属性集合。

泛型类

原文地址

在本教程中,你将学习开发 TypeScript 中的泛型类。

TypeScript 中的泛型类介绍

泛型 类的语法如下,泛型类型参数列表在尖括号 <> 中,类名之后:

class className<T> {
  //...
}

TypeScript 允许类型参数列表中存在多个泛型类型,如下所示:

class className<K, T> {
  //...
}

泛型约束 同样可以应用于类中的泛型类型:

class className<T extends TypeA> {
  //...
}

在类上放置类型参数允许你开发相同类型的方法和属性。

泛型类例子

在这个例子中,我们将开发一个 Stack 泛型类。

栈是一个基于后进先出 (LIFO) 原则的数据结构,这意味着第一个放入到栈中的元素会是栈中最后一个获取到的元素。

通常栈有大小的限制,默认为空,栈有两个主要的操作:

  • Push: 将一个元素推入到栈中
  • Pop: 从栈中弹出一个元素

下面演示一个完整的栈泛型类代码,名为 Stack<T>

class Stack<T> {
  private elements: T[] = [];

  constructor(private size: number) {}
  isEmpty(): boolean {
    return this.elements.length === 0;
  }
  isFull(): boolean {
    return this.elements.length === this.size;
  }
  push(element: T): void {
    if (this.elements.length === this.size) {
      throw new Error('The stack is overflow!');
    }
    this.elements.push(element);
  }
  pop(): T {
    if (this.elements.length == 0) {
      throw new Error('The stack is empty!');
    }
    return this.elements.pop();
  }
}

下面创建了一个数字栈:

let numbers = new Stack<number>(5);

下面的函数返回 lowhigh 两个数字之间的随机数:

function randBetween(low: number, high: number): number {
  return Math.floor(Math.random() * (high - low + 1) + low);
}

现在可以使用 randBetween() 函数生成随机数,然后推入到数字栈中:

let numbers = new Stack<number>(5);

while (!numbers.isFull()) {
  let n = randBetween(1, 10);
  console.log(`Push ${n} into the stack.`);
  numbers.push(n);
}

输出:

Push 3 into the stack.
Push 2 into the stack.
Push 1 into the stack.
Push 8 into the stack.
Push 9 into the stack.

下面演示从栈中弹出元素的操作,直到栈为空:

while (!numbers.isEmpty()) {
  let n = numbers.pop();
  console.log(`Pop ${n} from the stack.`);
}

输出:

Pop 9 from the stack.
Pop 8 from the stack.
Pop 1 from the stack.
Pop 2 from the stack.
Pop 3 from the stack.

同样的,你可以创建一个字符串栈,如下所示:

let words = 'The quick brown fox jumps over the lazy dog'.split(' ');

let wordStack = new Stack<string>(words.length);

// push words into the stack
words.forEach((word) => wordStack.push(word));

// pop words from the stack
while (!wordStack.isEmpty()) {
  console.log(wordStack.pop());
}

它是这样工作的:

  • 首先,把句子拆分成单词;
  • 然后,创建一个栈,大小等于单词数组的单词数量;
  • 第三,将单词数组中的单词逐个推入到栈中;
  • 最后,将栈中的单词弹出,直到栈为空。

泛型接口

原文地址

在本教程中,你将学习 TypeScript 中的泛型接口。

TypeScript 中的泛型接口介绍

和类一样,接口也支持泛型,泛型接口的语法如下:泛型类型参数列表在尖括号 <> 中,接口名称之后:

interface interfaceName<T> {
  // ...
}

这使得类型参数 T 对接口的所有成员都是可见。

类型参数列表可以是一个或者多个类型,如下所示:

interface interfaceName<U, V> {
  // ...
}

泛型接口例子:

看几个泛型接口声明的例子:

1) 描述对象属性的泛型接口

下面演示了一个泛型接口,他包含 keyvalue 两个属性,类型分别是 KV 类型:

interface Pair<K, V> {
  key: K;
  value: V;
}

现在,你可以使用 Pair 接口定义任意指定类型的键值对对象,比如:

let month: Pair<string, number> = {
  key: 'Jan',
  value: 1,
};

console.log(month);

在这个例子中,我们定义了一个 key 为字符串类型而 value 为数字类型的键值对对象。

2) 描述方法的泛型接口

下面声明了一个泛型接口,它有两个方法:add()remove() 方法:

interface Collection<T> {
  add(o: T): void;
  remove(o: T): void;
}

List<T> 泛型类实现了 Collection<T> 泛型接口:

class List<T> implements Collection<T> {
  private items: T[] = [];

  add(o: T): void {
    this.items.push(o);
  }
  remove(o: T): void {
    let index = this.items.indexOf(o);
    if (index > -1) {
      this.items.splice(index, 1);
    }
  }
}

对于 List<T> 类,你可以创建任意类型的值列表,如数字或者字符串类型。比如,下面演示如何使用 List<T> 泛型类来创建一个数字列表对象:

let list = new List<number>();

for (let i = 0; i < 10; i++) {
  list.add(i);
}

3) 描述索引类型的泛型接口

下面声明了一个描述索引类型的接口:

interface Options<T> {
  [name: string]: T;
}

let inputOptions: Options<boolean> = {
  disabled: false,
  visible: true,
};