软件工程的一个主要部分是构建组件,这些组件不仅具有定义明确且一致的 API,而且还可以重用。能够处理今天和明天的数据的组件将为您提供构建大型软件系统的最灵活的能力。
在 C# 和 Java 等语言中,工具箱中用于创建可重用组件的主要工具之一是泛型,也就是说,能够创建一个可以在多种类型而不是单一类型上工作的组件。这允许用户使用这些组件并使用他们自己的类型。
泛型类型变量
如果我们需要实现一个函数,并保证它的输入类型和输出类型保持一致
// 不使用泛型, arg可能需要添加更多的约束类型或则直接使用any(但我们为什么使用ts?)
function identity(arg: string | number): string | number {
return arg;
}
// 使用泛型
function identity<T>(arg: T): T {
return arg;
}
T向标识函数添加了一个类型变量,使我们能捕获用户提供的类型变量,方便我们再次使用该类型变量。
// 控制函数输入任意参数时返回该参数的元组类型
function toTuple<T>(arg: T): [T] {
return [arg];
}
泛型类型
泛型函数的类型和非泛型函数的类型一样,类型参数先列出,类似于函数声明
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甚至更复杂的对象。
就像接口一样,将类型参数放在类本身可以让我们确保类的所有属性都使用相同的类型。
一个类的类型有两个方面:静态方面和实例方面。泛型类仅在其实例方面而非其静态方面是通用的,因此在使用类时,静态成员不能使用类的类型参数。
通用约束
我们有时可能想要编写一个适用于一组类型的泛型函数,我们希望能够访问 的.length属性arg,但编译器无法证明每种类型都有一个.length属性,因此它警告我们不能做出这样的假设。
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.Property 'length' does not exist on type 'Type'.
return arg;
}
我们不想使用任何和所有类型,而是希望将此函数限制为使用也 具有该.length属性的任何和所有类型。只要类型有这个成员,我们就会允许它,但它至少需要有这个成员。为此,我们必须将我们的要求列为限制条件Type。
为此,我们将创建一个描述约束的接口。在这里,我们将创建一个具有单个.length属性的接口,然后我们将使用该接口和extends关键字来表示我们的约束
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length);
return arg;
}
// 因为泛型函数现在受到约束,它将不再适用于任何和所有类型
loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// 相反,我们需要传入其类型具有所有必需属性的值
loggingIdentity({ length: 10, value: 3 });
在泛型约束中使用类型参数
您可以声明受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外抓取 上不存在的属性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"'.
在泛型中使用类类型
在 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;