Typescript中泛型是什么?
TypeScript Generics是提供创建可重用组件的方法的工具。 它能够创建可以使用多种数据类型而不是单一数据类型的组件。 而且,它在不影响性能或生产率的情况下提供了类型安全性。 泛型允许我们创建泛型类,泛型函数,泛型方法和泛型接口。
在泛型中,类型参数写在左括号(<)和右括号(>)之间,这使它成为强类型集合。 它使用一种特殊的类型变量<T>来表示类型。
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("edureka");
let output2 = identity<number>( 117 );
console.log(output1);
console.log(output2);
首先,让我们看一个泛型的 “helloworld” 示例:identity
函数。identity
函数是一个返回传入内容的函数,类似于 echo
命令。
如果没有泛型,则必须为 identity
函数指定特定类型:
function identity(arg: number): number {
return arg;
}
或者,使用 any
类型描述标识函数:
function identity(arg: any): any {
return arg;
}
虽然使用 any
肯定是泛型的,因为它表示函数接受任何类型的 arg
,但函数返回时,我们丢失了关于该类型的信息。如果我们传入的是一个数字,我们得到的唯一信息是该函数可以返回任何类型。
我们真正需要的是一种捕获参数类型的方法,这样我们就可以使用它来表示返回的内容。在这里,我们将使用类型变量,一种特殊类型的变量,作用于类型而不是值。
function identity<Type>(arg: Type): Type {
return arg;
}
现在,我们在 identity
函数中添加了一个类型变量。Type
允许我们捕获用户提供的类型(例如 number
),以便我们以后使用该信息。这里,我们再次使用 Type
作为返回类型。我们现在可以看到参数和返回使用了相同的类型。这允许我们在函数的一侧传输该类型的信息,在另一侧输出该类型的信息。
我们说这个版本的 identity
函数是泛型的,因为它适用于一系列类型。与使用 any
不同,它与第一个使用 number
作为参数和返回类型的 identity
函数一样精确(即,它不会丢失任何信息)。
一旦我们编写了泛型标识函数,我们就可以用两种方法之一调用它。第一种方法是将所有参数(包括类型参数)传递给函数:
let output = identity<string>("myString");
// let output: string
在这里,我们显式地将 Type
设置为 string
,作为函数调用的参数之一,在参数周围使用 <>
而不是()
表示。
第二种方法也许也是最常见的。这里我们使用类型参数推断——也就是说,我们希望编译器根据传入的参数类型自动为我们设置Type
值:
let output = identity("myString");
// let output: string
虽然类型参数推断可以保持代码更短和更可读,但在更复杂的示例中,当编译器无法推断类型时,您可能需要显式地传递类型参数。
使用泛型 Type 变量
当您开始使用泛型时,您会注意到,当创建像 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
有这个成员。请记住,我们前面说过,这些类型变量代表任何和所有类型,因此使用此函数的人可能会传入一个没有 .length
成员的数字。
假设我们实际上打算让这个函数处理 Type
数组,而不是直接处理 Type
。因为我们使用的是数组,所以 .length
成员应该是可用的。我们可以将其描述为好像在创建一个数组:
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
您可以将 loggingIdentity
的类型理解为“通用函数 loggingIdentity
接受一个类型参数 Type
,和一个参数 arg
,参数 arg
是一个 Type
数组,并返回一个 Type
数组。”如果传入一个 number
数组,我们将返回一个 number
数组,因为 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;
}
您可能已经熟悉其他语言中的类型。在下一节中,我们将介绍如何创建自己的泛型类型,如 Array<Type>
。
泛型类型
在前面的部分中,我们创建了适用于一系列类型的通用 identity
函数。在本节中,我们将探讨函数本身的类型以及如何创建通用接口。
泛型函数的类型与非泛型函数的类型类似,首先列出类型参数,类似于函数声明:
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;
请注意,我们的示例内容已稍有不同。现在,我们没有描述泛型函数,而是拥有一个非泛型函数签名,它是泛型类型的一部分。当我们使用 GenericEntityfn
时,我们现在还需要指定相应的类型参数(这里是: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
类型。我们可以使用字符串或者更复杂的对象。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,将 type
参数放在类本身上可以确保类的所有属性都使用相同的类型。
正如我们在类一节中所述,类的类型有两个方面:静态方面和实例方面。泛型类仅在其实例端而不是静态端是泛型的,因此在处理类时,静态成员不能使用类的类型参数。
泛型约束
如果您还记得前面的一个示例,那么您有时可能希望编写一个通用函数,该函数在一组类型上工作,您对该组类型将具有的功能有一些了解。在我们的 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
属性的接口,然后使用此接口和 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 });
在泛型约束中使用类型参数
可以声明受其他类型参数约束的类型参数。例如,这里我们希望从给定名称的对象中获取属性。我们希望确保不会意外获取 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 类型
当在 TypeScript 中使用泛型创建工厂时,有必要通过类的构造函数引用 class 类型。例如:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
更高级的示例使用 prototype
属性来推断和约束构造函数和 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();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;