本文深入解析TypeScript中的泛型机制,涵盖恒等函数、类型参数推断、泛型接口与类的创建,以及泛型约束的应用。通过具体代码示例,展示如何利用类型变量保持类型信息,并通过extends关键字限制类型范围,提升代码复用性与安全性。
引言
尽管TypeScript官方文档已持续更新,但目前可查的中文资料多基于旧版本。因此,本文对新增及修订较多的章节进行了翻译与整理。
本内容主要源自TypeScript官方手册中的“泛型(Generics)”章节。
本文并非逐字翻译,而是对部分关键内容进行了解释与补充说明。
泛型初探(Hello World of Generics)
软件工程的关键环节之一是构建组件。优秀的组件不仅需要具备定义清晰且一致的API,还必须具备可复用性(reusable)。高质量的组件应能兼容当前的数据类型,并适应未来可能出现的新类型,这在构建大型软件系统时能提供极大的灵活性。
在C#和Java等语言中,用于创建可复用组件的机制被称为泛型(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中文手册 https://typescript.bootcss.com/
TypeScript系列文章由官方文档翻译、重难点解析、实战技巧三部分组成,涵盖入门、进阶、实战,旨在为您提供系统学习TS的教程。更多关于TypeScript Generics泛型的资料请关注本站其他相关文章!