软件工程主要的一个部分就是创建不仅有良好定义还有统一的API的组件,并且还需要具有可复用性。能够处理多种数据的组件可以为构建大型软件系统提供最灵活的功能。
在C#和Java中,主要用于创建可复用组件的方式是泛型(generics),这种方式能够创建一个可以在多种类型上工作的组件,而不是单个。这允许用户去定义自己的类型来使用这些组件。
你好泛型(Hello World of Generics)
我们先写一个hello world的泛型:identity函数。identity函数是一个返回你传入参数的函数。你可以认为它和echo类似。
不使用泛型的话,我们要给函数一个特定的类型:
function identity(arg: number): number {
return arg;
}
或者,我们可以使用any类型:
function identity(arg: any): any {
return arg;
}
当然使用any类型也是泛型,因为函数将可以接受任何类型的arg,但是我们丢失了返回类型。假如我们传递一个number类型,那我们只能知道一个any类型被返回了。
实际上,我们需要一种可以捕获类型参数的方法,这样我们就可以用它来表示返回的内容了。
因此,我们可以使用一个类型变量(type variable),一种在类型上工作而不是值量。
function identity<Type>(arg: Type): Type {
return arg;
}
现在我们添加了一个类型参数Type到identity函数当中了,Type允许我们获得用户提供的类型。之后,我们再次使用Type在返回类型之后。通过观察,我们现在可以看到返回的类型和参数的类型是相同的。
我们可以看到这个版本的identity函数是泛型版本,它可以使用任意类型。与any不同的是,泛型版本和第一个使用数字作为参数和返回类型的标识函数一样精确。
一旦我们写了泛型版本的identity函数,我们可以使用两种方式来调用它。第一种方式是给泛型函数传递类型参数:
let output = identity<string>("myString");
// let output: string
这里我们使用<>显式地将Type设置为string。
第二种方式大概是是更多被使用的。这里我们使用类型参数推导(type argument inference),这种方式可以让编译器自动设置Type基于我们传递的参数的类型:
let output = identity("myString");
// let output: string
注意我们没有显式的使用<>,编译器根据"myString"来设置Type的类型。使用类型参数对保持代码简短和可读性有非常大的帮助,你可能需要显式地传递类型参数,就像之前编译器无法推断类型时所做的那样。
泛型应用(Working with Generic Typed)
当你开始使用泛型时,你会注意到你会创建像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数性的时候,请记住,实际上我们可以传入所有类型,所以使用这个函数的人,可能传入的是一个没有length属性的值。
实际上可以使用Type[]来代替Type来让这个函数正常工作。我们使用Type[]参数变量,length属性则会起效:
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
你可以将logginIdentity理解为“通用函数loggingIdentity接受一个类型参数Type,一个参数arg是一个类型数组,并返回一个类型数组”。如果我们传递一个数字类型的数组,函数会返回一个数组,就像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)
在前面那个章节,我们创建了可以在一系列类型上工作的identity函数。接下来,我们将来探索函数本身的类型和如何去创建泛型接口(generic interfaces)。
泛型函数看起来就像无泛型函数,类型参数被列在前面,看起和函数声明很像:
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。我们把字面量对象类型移动到interface中:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
在类似的例子当中,我们或许想要把泛型参数变成interface的一个参数。这使得interface的成员都可以访问类型参数。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
请注意两个例子有稍微的不同。
泛型类(Generic Classes)
一个泛型类和泛型interface很类似。泛型类的类型参数在名字后面用<>符号包裹起来。
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"));
就如interface一样,把类型参数放到类本身来让我们确保类的所有属性都使用相同的类型。
泛型约束
有时你可能想要去编写一个适用于一组类型的通用类型。在我们的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属性。只要这个类型有length属性,就能够被使用。为了做到这个,我们必须给Type一个约束。
因此,我们创建一个只有length属性的interface,并且我们使用这个interface和extends关键字来实现我们的约束:
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"'.
在泛型中使用class类型(Using Class Types in Generics)
当在TypeScript使用泛型创建函数工厂,那就有必要通过它们的构造函数来引用class类型。举个例子:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
一个更高级的示例----使用原型来推断和约束构造函数并且描述和class类型的实例之间的关系。
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();
}
// 约束和描述传参和Animal之间的关系
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
翻译自Generics