- 阐明:目前网上没有 TypeScript 最新官网文档的中文翻译,所以有了这么一个翻译打算。因为我也是 TypeScript 的初学者,所以无奈保障翻译百分之百精确,若有谬误,欢送评论区指出;
- 翻译内容:暂定翻译内容为 TypeScript Handbook,后续有空会补充翻译文档的其它局部;
- 我的项目地址:TypeScript-Doc-Zh,如果对你有帮忙,能够点一个 star ~
本章节官网文档地址:Generics
泛型
软件工程的一个次要局部是构建组件。组件不仅具备定义良好且统一的 API,而且还具备可重用性。组件如果既能解决当初的数据,又能解决未来的数据,那么它将在构建大型软件系统的时候为你提供最灵便的能力。
在 C# 和 Java 等语言中,创立可重用组件的次要工具之一就是泛型。利用泛型,咱们能够创立出实用于多种类型而不是繁多类型的组件,从而容许用户在应用这些组件的时候用上本人的类型。
初识泛型
初识泛型,让咱们先来实现一个 identity
函数。identity
函数能够承受任意参数并将其返回,你能够认为它的性能相似于 echo
指令。
如果不应用泛型,那么咱们必须给 identity
函数指定一个具体的类型:
function identity(arg: number): number { return arg;}
或者,咱们能够应用 any
类型去形容 identity
函数:
function identity(arg: any): any { return arg;}
应用 any
的确是一种通用的做法,因为函数的 arg
参数能够承受任意类型或者所有类型的值,但实际上,咱们失落了函数返回值类型的信息。如果咱们传递的参数是数字,那么咱们惟一能晓得的信息是函数能够返回任意类型的值。
相同,咱们须要一种办法去捕捉参数的类型,这样咱们也能够应用它来示意返回值的类型。这里,咱们会应用一个类型变量,这种非凡的变量作用于类型而非值。
function identity<Type>(arg: Type): Type { return arg;}
咱们当初给 identity
函数增加了类型变量 Type
。Type
容许咱们捕捉用户传入参数的类型(比如说 number
类型),这将作为稍后能够利用的信息。接着,咱们在返回值类型的申明中也应用了 Type
。从代码能够看出,参数和返回值都应用了雷同的类型,这能够让咱们在函数的一侧承受类型信息,并将信息传输给另一侧作为函数的输入。
咱们称这个版本的 identity
函数是一个泛型函数,因为它实用于一系列的类型。和应用 any
不同,这个函数十分地明确(比如说,它没有失落任何类型信息),成果和之前应用 number
作为参数和返回值类型的那个 identity
函数一样。
一旦实现了泛型的 identity
函数,咱们就能够通过两种形式去调用它。第一种形式是给函数传递所有参数,包含类型参数:
let output = identity<string>("myString"); ^^^^^ // let output: string
这里,咱们显式设置 Type
为 string
并将其作为函数调用的参数。留神包裹参数的是 <>
不是 ()
。
第二种形式可能是最罕用的。咱们在这里应用了类型参数推断 —— 也就是说,咱们想要让编译器基于传入参数的类型主动设置 Type
的值:
let output = identity("myString"); ^^^^^ // let output: string
留神,咱们并不一定要在 <>
中显式传入类型。编译器会查看值 myString
,并将这个值的类型作为 Type
的值。尽管类型参数推断能够无效地保障代码的简洁与可读性,但在更简单的案例中,编译器可能无奈胜利推断出类型,这时候你就须要像之前那样显式传入类型参数了。
应用泛型的类型变量
在你开始应用泛型之后,你会留神到,每次你创立相似 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
类型的值进去,而这种值是没有 .length
成员的。
假如咱们实际上想要让这个函数解决 Type
类型的数组,而不是间接解决 Type
。既然以后解决的是数组,那么它就必定有 .length
成员了。咱们能够像创立其它类型的数组一样进行形容:
function loggingIdentity<Type>(arg: Type[]): Type[] { console.log(arg.length); return arg;}
你能够将 loggingIdentity
的类型解读为“泛型函数 loggingIdentity
承受一个类型参数 Type
,以及一个参数 arg
,后者是一个 Type
类型的数组。函数最初返回的也是一个 Type
类型的数组”。如果咱们传入的是 number
类型的数组,那么最终返回的也是 number
类型的数组,因为 Type
会绑定为 number
。这容许咱们将泛型类型变量 Type
作为要用到的其中一种类型去应用,而不是作为整个类型去应用,因而给予了咱们更大的灵活性。
咱们也能够将这个简略的示例改写为如下:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> { console.log(arg.length); // 数组有 length 属性,所以不会再抛出谬误 return arg;}
你可能曾经很相熟其它语言中的这种类型格调了。在下一节中,咱们会介绍如何本人创立相似 Array<Type>
这样的泛型。
泛型类型
在后面几节中,咱们创立了实用于一系列类型的泛型函数 identity
。在本节中,咱们将摸索函数自身的类型以及创立泛型接口的办法。
泛型函数类型和非泛型函数类型一样,只是前者会像泛型函数申明一样先列举出类型参数:
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
),从而无效地保障它被底层的函数签名所应用。了解什么时候把类型参数间接放到调用签名中,什么时候把类型参数放到接口自身中,对于形容某个类型的哪些地方是泛型有很大的作用。
除了泛型接口以外,咱们也能够创立泛型类。留神咱们无奈创立泛型枚举和命名空间。
泛型类
泛型类和泛型接口的构造很类似。泛型类在类名前面会跟着 <>
,外面是一个泛型类型参数列表。
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;};
这种应用 GenericNumber
类的办法十分平实,但你可能留神到了一件事件,那就是咱们并没有限度类只能应用 number
类型。相同,咱们能够应用 string
甚至是其它更简单的对象。
let stringNumeric = new GenericNumber<string>();stringNumeric.zeroValue = "";stringNumeric.add = function (x, y) { return x + y;}; console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
和接口一样,把类型参数放到类自身,能够让咱们确保类的所有属性都在应用同一类型。
咱们在对于类的章节有提到过,类蕴含两局部:动态局部和实例局部。泛型类的泛型只实用于实例局部而非动态局部,因而在应用泛型类的时候,动态成员无奈应用类的类型参数,
泛型束缚
还记得之前拜访参数长度的例子吗?有时候,你可能须要编写一个只作用于某些类型的泛型函数,并且你对这些类型的特色有肯定的理解。比方在示例的 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
的要求,以束缚 Type
的类型。
为此,咱们会创立一个形容约束条件的接口。这里,咱们创立了一个具备单属性 length
的接口,之后应用该接口和 extends
关键字去示意咱们的束缚:
interface Lengthwise { length: number;} function loggingIdentity<Type extends Lengthwise>(arg: Type): Type { console.log(arg.length); // 当初咱们晓得它肯定是有 length 属性的,所以不会抛出谬误 return arg;}
因为泛型函数当初受到了束缚,所以它不再能够解决任意的、所有的类型:
loggingIdentity(3); ^// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
相同,咱们传入的值的类型应该具备所有必须属性:
loggingIdentity({ length: 10, value: 3 });
在泛型束缚中应用类型参数
你能够申明一个类型参数,让它受到另一个类型参数的束缚。举个例子,当初咱们须要通过给定属性名拜访对象的属性,那么咱们必须确保不会意外地拜访对象上不存在的属性,因而咱们就会在两个类型之间应用一个束缚:
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"'.
在泛型中应用类类型
在应用 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;
这种模式能够用于驱动混入设计模式。