TypeScript 之 Generics

2021年11月22日 阅读数:8
这篇文章主要向大家介绍TypeScript 之 Generics,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前言

TypeScript 的官方文档早已更新,但我能找到的中文文档都还停留在比较老的版本。因此对其中新增以及修订较多的一些章节进行了翻译整理。typescript

本篇整理自 TypeScript Handbook 中 「Generics」 章节。segmentfault

本文并不严格按照原文翻译,对部份内容也作了解释补充。数组

正文

软件工程的一个重要部分就是构建组件,组件不只须要有定义良好和一致的 API,也须要是可复用的(reusable)。好的组件不只可以兼容今天的数据类型,也能适用于将来可能出现的数据类型,这在构建大型软件系统时会给你最大的灵活度。微信

在好比 C# 和 Java 语言中,用来建立可复用组件的工具,咱们称之为泛型(generics)。利用泛型,咱们能够建立一个支持众多类型的组件,这让用户可使用本身的类型消费(consume)这些组件。ide

Generics 初探(Hello World of Generics)

让咱们开始写第一个泛型,一个恒等函数(identity function)。所谓恒等函数,就是一个返回任何传进内容的函数。你也能够把它理解为相似于 echo 命令。函数

不借助泛型,咱们也许须要给予恒等函数一个具体的类型:工具

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

或者,咱们使用 any 类型:翻译

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

尽管使用 any 类型可让咱们接受任何类型的 arg 参数,但也让咱们丢失了函数返回时的类型信息。若是咱们传入一个数字,咱们惟一知道的信息是函数能够返回任何类型的值。code

因此咱们须要一种能够捕获参数类型的方式,而后再用它表示返回值的类型。这里咱们用了一个类型变量(type variable),一种用在类型而非值上的特殊的变量。对象

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

如今咱们已经给恒等函数加上了一个类型变量 Type,这个 Type 容许咱们捕获用户提供的类型,使得咱们在接下来可使用这个类型。这里,咱们再次用 Type 做为返回的值的类型。在如今的写法里,咱们能够清楚的知道参数和返回值的类型是同一个。

如今这个版本的恒等函数就是一个泛型,它能够支持传入多种类型。不一样于使用 any,它没有丢失任何信息,就跟第一个使用 number 做为参数和返回值类型的的恒等函数同样准确。

在咱们写了一个泛型恒等函数后,咱们有两种方式能够调用它。第一种方式是传入全部的参数,包括类型参数:

let output = identity<string>("myString"); // let output: string

在这里,咱们使用 <> 而不是 ()包裹了参数,并明确的设置 Typestring 做为函数调用的一个参数。

第二种方式可能更常见一些,这里咱们使用了类型参数推断(type argument inference)(部分中文文档会翻译为“类型推论”),咱们但愿编译器能基于咱们传入的参数自动推断和设置 Type 的值。

let output = identity("myString"); // let output: string

注意此次咱们并无用 <> 明确的传入类型,当编译器看到 myString 这个值,就会自动设置 Type 为它的类型(即 string)。

类型参数推断是一个颇有用的工具,它可让咱们的代码更短更易阅读。而在一些更加复杂的例子中,当编译器推断类型失败,你才须要像上一个例子中那样,明确的传入参数。

使用泛型类型变量(Working with Generic Type Variables)

当你建立相似于 identity 这样的泛型函数时,你会发现,编译器会强制你在函数体内,正确的使用这些类型参数。这就意味着,你必须认真的对待这些参数,考虑到他们多是任何一个,甚至是全部的类型(好比用了联合类型)。

让咱们以 identity 函数为例:

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

若是咱们想打印 arg 参数的长度呢?咱们也许会尝试这样写:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length);
    // Property 'length' does not exist on type 'Type'.
  return arg;
}

若是咱们这样作,编译器会报错,提示咱们正在使用 arg.length属性,可是咱们却没有在其余地方声明 arg 有这个属性。咱们前面也说了这些类型变量表明了任何甚至全部类型。因此彻底有可能,调用的时候传入的是一个 number 类型,可是 number 并无 .length 属性。

如今假设这个函数,使用的是 Type 类型的数组而不是 Type。由于咱们使用的是数组,.length 属性确定存在。咱们就能够像建立其余类型的数组同样写:

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

你能够这样理解 loggingIdentity 的类型:泛型函数 loggingIdentity 接受一个 Type 类型参数和一个实参 arg,实参 arg 是一个 Type 类型的数组。而该函数返回一个 Type 类型的数组。

若是咱们传入的是一个全是数字类型的数组,咱们的返回值一样是一个全是数字类型的数组,由于 Type 会被当成 number 传入。

如今咱们使用类型变量 Type,是做为咱们使用的类型的一部分,而不是以前的一整个类型,这会给咱们更大的自由度。

咱们也能够这样写这个例子,效果是同样的:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}

泛型类型 (Generic Types)

在上个章节,咱们已经建立了一个泛型恒等函数,能够支持传入不一样的类型。在这个章节,咱们探索函数自己的类型,以及如何建立泛型接口。

泛型函数的形式就跟其余非泛型函数的同样,都须要先列一个类型参数列表,这有点像函数声明:

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

泛型的类型参数可使用不一样的名字,只要数量和使用方式上一致便可:

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

咱们也能够以对象类型的调用签名的形式,书写这个泛型类型:

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

这能够引导咱们写出第一个泛型接口,让咱们使用上个例子中的对象字面量,而后把它的代码移动到接口里:

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

有的时候,咱们会但愿将泛型参数做为整个接口的参数,这可让咱们清楚的知道传入的是什么参数 (举个例子:Dictionary<string> 而不是 Dictionary)。并且接口里其余的成员也能够看到。

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

注意在这个例子里,咱们只作了少量改动。再也不描述一个泛型函数,而是将一个非泛型函数签名,做为泛型类型的一部分。

如今当咱们使用 GenericIdentityFn 的时候,须要明确给出参数的类型。(在这个例子中,是 number),有效的锁定了调用签名使用的类型。

当要描述一个包含泛型的类型时,理解何时把类型参数放在调用签名里,何时把它放在接口里是颇有用的。

除了泛型接口以外,咱们也能够建立泛型类。注意,不可能建立泛型枚举类型和泛型命名空间。

泛型类(Generic Classes)

泛型类写法上相似于泛型接口。在类名后面,使用尖括号中 <> 包裹住类型参数列表:

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

在这个例子中,并无限制你只能使用 number 类型。咱们也可使用 string 甚至更复杂的类型:

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

就像接口同样,把类型参数放在类上,能够确保类中的全部属性都使用了相同的类型。

正如咱们在 Class 章节提过的,一个类它的类型有两部分:静态部分和实例部分。泛型类仅仅对实例部分生效,因此当咱们使用类的时候,注意静态成员并不能使用类型参数。

泛型约束(Generic Constraints)

在早一点的 loggingIdentity 例子中,咱们想要获取参数 arg.length 属性,可是编译器并不能证实每种类型都有 .length 属性,因此它会提示错误:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length);
  // Property 'length' does not exist on type 'Type'.
  return arg;
}

相比于能兼容任何类型,咱们更愿意约束这个函数,让它只能使用带有 .length 属性的类型。只要类型有这个成员,咱们就容许使用它,但必须至少要有这个成员。为此,咱们须要列出对 Type 约束中的必要条件。

为此,咱们须要建立一个接口,用来描述约束。这里,咱们建立了一个只有 .length 属性的接口,而后咱们使用这个接口和 extend关键词实现了约束:

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

如今这个泛型函数被约束了,它再也不适用于全部类型:

loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

咱们须要传入符合约束条件的值:

loggingIdentity({ length: 10, value: 3 });

在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)

你能够声明一个类型参数,这个类型参数被其余类型参数约束。

举个例子,咱们但愿获取一个对象给定属性名的值,为此,咱们须要确保咱们不会获取 obj 上不存在的属性。因此咱们在两个类型之间创建一个约束:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
getProperty(x, "m");

// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

在泛型中使用类类型(Using Class Types in Generics)

在 TypeScript 中,当使用工厂模式建立实例的时候,有必要经过他们的构造函数推断出类的类型,举个例子:

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

下面是一个更复杂的例子,使用原型属性推断和约束,构造函数和类实例的关系。

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

TypeScript 系列

  1. TypeScript 之 Narrowing
  2. TypeScript 之 More on Functions
  3. TypeScript 之 Object Type

若是你对于 TypeScript 有什么困惑或者其余想要了解的内容,欢迎与我交流,微信:「mqyqingfeng」,公众号搜索:「冴羽的JavaScript博客」或者「yayujs」

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。