# 五、泛型
👍 star本项目给作者一点鼓励吧
# 1.泛型(Generics)
让我们开始写第一个泛型,一个恒等函数(identity function)。所谓恒等函数,就是一个返回任何传进内容的函数。
function identity(arg: number): number {
return arg; // arg是number类型,返回值同样是number类型
}
我们需要一种可以捕获参数类型的方式,然后再用它表示返回值的类型。这里我们用了一个类型变量(type variable),一种用在类型而非值上的特殊的变量。
function identity<Type>(arg: Type): Type {
return arg;
}
现在我们已经给恒等函数加上了一个类型变量Type
,这个Type
允许我们捕获用户提供的类型,使得我们在接下来可以使用这个类型。这里,我们再次用Type
作为返回的值的类型。在现在的写法里,我们可以清楚的知道参数和返回值的类型是同一个。
在我们写了一个泛型恒等函数后,我们有两种方式可以调用它。第一种方式是传入所有的参数,包括类型参数:
const identityString = identity<string> // const res: (arg: string) => string
第二种方式可能更常见一些,这里我们使用了类型参数推断(type argument inference)我们希望编译器能基于我们传入的参数自动推断和设置Type
的值。
let strA = identity('isString'); // let strA: string
注意这次我们并没有用<Type>
明确的传入类型,当编译器看到isString
这个值,就会自动设置Type
为它的类型(即 string)。
类型参数推断是一个很有用的工具,它可以让我们的代码更短更易阅读。而在一些更加复杂的例子中,当编译器推断类型失败,你才需要像上一个例子中那样,明确的传入参数。
# 2.使用泛型类型变量(Working with Generic Type Variables)
我们在上一节了解到的例子中,Type是直接作为类型进行传递的,而在某些时候Type也可以是类型的某一个参数,例如:
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
console.log(arg[0]);
return arg;
}
泛型函数loggingIdentity
接受一个 Type 类型参数和一个实参arg
,实参arg
是一个Type
类型的数组。而该函数返回一个Type
类型的数组。
现在我们使用类型变量 Type,是作为我们使用的类型的一部分,而不是之前的一整个类型,这会给我们更大的自由度。
# 3.泛型类型 (Generic Types)
泛型函数的形式就跟其他非泛型函数的一样,都需要先列一个类型参数列表,这有点像函数声明:
function identity<Type>(arg: Type): Type {
console.log('>>:', arg);
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
注意:不要对<T>
感到疑惑,泛型的类型参数(Type || T || XX)可以使用不同的名字,只要数量和使用方式上一致即可。
我们也可以以对象类型的调用签名的形式,书写这个泛型类型:
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;
# 4.泛型类(Generic Classes)
泛型类写法上类似于泛型接口。在类名后面,使用尖括号中 <> 包裹住类型参数列表:
class GenericNumber<NumType> {
zeroValue: NumType
add: (x: NumType,) => NumType = x => x
constructor(x: NumType) {
this.zeroValue = x;
}
}
const data = new GenericNumber(1);
data.add; // (property) GenericNumber<number>.add: (x: number) => number
在这个例子中,并没有限制你只能使用number
类型。我们也可以使用其他类型:
type strOrNum = string | number;
class GenericNumber<NumType> {
zeroValue: NumType
add: (x: NumType,) => NumType = x => x
constructor(x: NumType) {
this.zeroValue = x;
}
}
const data = new GenericNumber<strOrNum>(1);
data.add; // (property) GenericNumber<strOrNum>.add: (x: strOrNum) => strOrNum
就像接口一样,把类型参数放在类上,可以确保类中的所有属性都使用了相同的类型。
# 5.泛型约束(Generic Constraints)
在早一点的 第三章函数中 我们想要获取参数obj.length
属性,但是编译器并不能证明每种类型都有 .length
属性,相比于能兼容任何类型,我们更愿意约束这个函数,让它能使用带有.length
属性的类型。只要类型有这个成员,我们就允许使用它,但必须至少要有这个成员。
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
我们也可以列出对 Type 约束中的必要条件,为此,我们需要创建一个接口,用来描述约束。这里,我们创建了一个只有 .length 属性的接口,然后我们使用这个接口和 extend关键词实现了约束:
interface Lengthwise {
length: number;
}
function longest<Type extends Lengthwise>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
# 5.1 在泛型约束中使用类型参数(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");
// 类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数。
# 5.2 在泛型中使用类类型(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;
# 写在最后
参考:
欢迎Star⭐️