# 五、泛型

👍 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⭐️

Last Updated: 12/5/2022, 9:45:07 PM