前言
TypeScript 的官网文档早已更新,但我能找到的中文文档都还停留在比拟老的版本。所以对其中新增以及订正较多的一些章节进行了翻译整顿。
本篇整顿自 TypeScript Handbook 中 「Generics」 章节。
本文并不严格依照原文翻译,对局部内容也做了解释补充。
注释
软件工程的一个重要局部就是构建组件,组件不仅须要有定义良好和统一的 API,也须要是可复用的(reusable)。好的组件不仅可能兼容明天的数据类型,也能实用于将来可能呈现的数据类型,这在构建大型软件系统时会给你最大的灵便度。
在比方 C# 和 Java 语言中,用来创立可复用组件的工具,咱们称之为泛型(generics)。利用泛型,咱们能够创立一个反对泛滥类型的组件,这让用户能够应用本人的类型生产(consume)这些组件。
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
参数,但也让咱们失落了函数返回时的类型信息。如果咱们传入一个数字,咱们惟一晓得的信息是函数能够返回任何类型的值。
所以咱们须要一种能够捕捉参数类型的形式,而后再用它示意返回值的类型。这里咱们用了一个类型变量(type variable),一种用在类型而非值上的非凡的变量。
function identity<Type>(arg: Type): Type { return arg;}
当初咱们曾经给恒等函数加上了一个类型变量 Type
,这个 Type
容许咱们捕捉用户提供的类型,使得咱们在接下来能够应用这个类型。这里,咱们再次用 Type
作为返回的值的类型。在当初的写法里,咱们能够分明的晓得参数和返回值的类型是同一个。
当初这个版本的恒等函数就是一个泛型,它能够反对传入多种类型。不同于应用 any
,它没有失落任何信息,就跟第一个应用 number
作为参数和返回值类型的的恒等函数一样精确。
在咱们写了一个泛型恒等函数后,咱们有两种形式能够调用它。第一种形式是传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // let output: string
在这里,咱们应用 <>
而不是 ()
包裹了参数,并明确的设置 Type
为 string
作为函数调用的一个参数。
第二种形式可能更常见一些,这里咱们应用了类型参数推断(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 系列
- TypeScript 之 Narrowing
- TypeScript 之 More on Functions
- TypeScript 之 Object Type
如果你对于 TypeScript 有什么困惑或者其余想要理解的内容,欢送与我交换,微信:「mqyqingfeng」,公众号搜寻:「冴羽的JavaScript博客」或者「yayujs」
如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。