泛型 - TypeScript 创建类型
泛型
软件工程的一个重要部分就是构建组件,不仅具有定义良好且一致的 API,而且可重用的组件。好的组件不仅能够兼容现今的数据类型,也能适用于未来可能出现的数据类型,这在构建大型软件系统时会给你最大的灵活度。
在像 C#和 Java 这样的语言中,工具箱中用于创建可重用组件的主要工具之一是泛型,也就是说,能够创建一个可以处理多种类型而不是单个类型的组件。这允许用户使用这些组件并使用自己的类型。
泛型初探
让我们开始写第一个泛型,一个恒等函数(identity function)。所谓恒等函数,就是一个返回任何传进内容的函数。你也可以把它理解为类似于echo
命令。
不借助泛型,我们也许需要给予恒等函数一个具体的类型:
function identity(arg: number): number { return arg; }
或者,我们使用any
类型:
function identity(arg: any): any { return arg; }
尽管使用any
类型可以让我们接受任何类型的arg
参数,但也让我们丢失了函数返回时的类型信息。如果我们传入一个数字,我们唯一知道的信息是函数可以返回任何类型的值。
所以我们需要一种可以捕获参数类型的方式,然后再用它表示返回值的类型。这里我们用了一个类型变量(type variable),一种用在类型而非值上的特殊的变量。
function identity(arg: Type): Type { return arg; }
现在我们已经给恒等函数加上了一个类型变量Type
,这个Type
允许我们捕获用户提供的类型,使得我们在接下来可以使用这个类型。这里,我们再次用Type
作为返回的值的类型。在现在的写法里,我们可以清楚的知道参数和返回值的类型是同一个。
现在这个版本的恒等函数就是一个泛型,它可以支持传入多种类型。不同于使用any
,它没有丢失任何信息,就跟第一个使用number
作为参数和返回值类型的的恒等函数一样准确。
在我们写了一个泛型恒等函数后,我们有两种方式可以调用它。
第一种方式是传入所有的参数,包括类型参数:
let output = identity("myString"); // let output: string
在这里,我们使用而不是
()
包裹了参数,并明确的设置Type
为string
作为函数调用的一个参数。
第二种方式可能更常见一些,这里我们使用了类型参数推断(type argument inference),我们希望编译器能基于我们传入的参数自动推断和设置Type
的值。
let output = identity("myString"); // let output: string
注意这次我们并没有用明确的传入类型,当编译器看到
myString
这个值,就会自动设置Type
为它的类型(即string
)。
类型参数推断是一个很有用的工具,它可以让我们的代码更短更易阅读。而在一些更加复杂的例子中,当编译器推断类型失败,你才需要像上一个例子中那样,明确的传入参数。
使用泛型类型变量
当你创建类似于identity
这样的泛型函数时,你会发现,编译器会强制你在函数体内,正确的使用这些类型参数。这就意味着,你必须认真的对待这些参数,考虑到他们可能是任何一个,甚至是所有的类型(比如用了联合类型)。
让我们以identity
函数为例:
function identity(arg: Type): Type { return arg; }
如果我们想打印arg
参数的长度呢?我们也许会尝试这样写:
function loggingIdentity(arg: Type): Type { console.log(arg.length); // Property 'length' does not exist on type 'Type'. return arg; }
如果我们这样做,编译器会报错,提示我们正在使用arg
的.length
属性,但是我们却没有在其他地方声明arg
有这个属性。我们前面也说了这些类型变量代表了任何甚至所有类型。所以完全有可能,调用的时候传入的是一个number
类型,但是number
并没有.length
属性。
现在假设这个函数,使用的是Type
类型的数组,.length
属性肯定存在。我们就可以像创建其他类型的数组一样写:
function loggingIdentity(arg: Type[]): Type[] { console.log(arg.length); return arg; }
你可以这样理解loggingIdentity
的类型:泛型函数loggingIdentity
接受一个Type
类型参数和一个实参arg
,实参arg
是一个Type
类型的数组。而该函数返回一个Type
类型的数组。
如果我们传入的是一个全是数字类型的数组,我们的返回值同样是一个全是数字类型的数组,因为Type
会被当成number
传入。
现在我们使用类型变量Type
,是作为我们使用的类型的一部分,而不是之前的一整个类型,这会给我们更大的自由度。
我们也可以这样写这个例子,效果是一样的:
function loggingIdentity(arg: Array): Array { console.log(arg.length); // Array has a .length, so no more error return arg; }
泛型类型
在上个章节,我们已经创建了一个泛型恒等函数,可以支持传入不同的类型。在这个章节,我们探索函数本身的类型,以及如何创建泛型接口。
泛型函数的形式就跟其他非泛型函数的一样,都需要先列一个类型参数列表,这有点像函数声明:
function identity(arg: Type): Type { return arg; } let myIdentity: (arg: Type) => Type = identity;
泛型的类型参数可以使用不同的名字,只要数量和使用方式上一致即可:
function identity(arg: Type): Type { return arg; } let myIdentity: (arg: Input) => Input = identity;
我们也可以以对象类型的调用签名的形式,书写这个泛型类型:
function identity(arg: Type): Type { return arg; } let myIdentity: { (arg: Type): Type } = identity;
这可以引导我们写出第一个泛型接口,让我们使用上个例子中的对象字面量,然后把它的代码移动到接口里:
interface GenericIdentityFn { (arg: Type): Type; } function identity(arg: Type): Type { return arg; } let myIdentity: GenericIdentityFn = identity;
有的时候,我们会希望将泛型参数作为整个接口的参数,这可以让我们清楚的知道传入的是什么参数(举个例子:Dictionary
而不是Dictionary
)。而且接口里其他的成员也可以看到。
interface GenericIdentityFn { (arg: Type): Type; } function identity(arg: Type): Type { return arg; } let myIdentity: GenericIdentityFn = identity;
注意在这个例子里,我们只做了少许改动。不再描述一个泛型函数,而是将一个非泛型函数签名,作为泛型类型的一部分。
现在当我们使用GenericIdentityFn
的时候,需要明确给出参数的类型。(在这个例子中,是number
),有效的锁定了调用签名使用的类型。
当要描述一个包含泛型的类型时,理解什么时候把类型参数放在调用签名里,什么时候把它放在接口里是很有用的。
除了泛型接口之外,我们也可以创建泛型类。注意,不可能创建泛型枚举类型和泛型命名空间。
泛型类
泛型类写法上类似于泛型接口。在类名后面,使用尖括号中包裹住类型参数列表:
class GenericNumber { zeroValue: NumType; add: (x: NumType, y: NumType) => NumType; } let myGenericNumber = new GenericNumber(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function (x, y) { return x + y; };
在这个例子中,并没有限制你只能使用number
类型。我们也可以使用string
甚至更复杂的类型:
let stringNumeric = new GenericNumber(); stringNumeric.zeroValue = ""; stringNumeric.add = function (x, y) { return x + y; }; console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
就像接口一样,把类型参数放在类上,可以确保类中的所有属性都使用了相同的类型。
正如我们在 Class 章节提过的,一个类它的类型有两部分:静态部分和实例部分。泛型类仅仅对实例部分生效,所以当我们使用类的时候,注意静态成员并不能使用类型参数。
泛型约束
在早一点的loggingIdentity
例子中,我们想要获取参数arg
的.length
属性,但是编译器并不能证明每种类型都有.length
属性,所以它会提示错误:
function loggingIdentity(arg: Type): Type { console.log(arg.length); // Property 'length' does not exist on type 'Type'.return arg; }
相比于能兼容任何类型,我们更愿意约束这个函数,让它只能使用带有.length
属性的类型。只要类型有这个成员,我们就允许使用它,但必须至少要有这个成员。为此,我们需要列出对Type
约束中的必要条件。
为此,我们需要创建一个接口,用来描述约束。这里,我们创建了一个只有.length
属性的接口,然后我们使用这个接口和extends
关键词实现了约束:
interface Lengthwise { length: number; } function loggingIdentity(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 });
在泛型约束中使用类型参数
你可以声明一个类型参数,这个类型参数被其他类型参数约束。
举个例子,我们希望获取一个对象给定属性名的值,为此,我们需要确保我们不会获取obj
上不存在的属性。所以我们在两个类型之间建立一个约束:
function getProperty(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(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(c: new () => A): A { return new c(); } createInstance(Lion).keeper.nametag; createInstance(Bee).keeper
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!