Typescript类型操作(泛型)

47 阅读5分钟

定义

软件工程的一个主要部分是构建组件,这些组件不仅具有定义明确且一致的 API,而且还可以重用。能够处理今天和明天的数据的组件将为你提供构建大型软件系统的最灵活的能力。

在 C# 和 Java 等语言中,工具箱中用于创建可重用组件的主要工具之一是泛型,也就是说,能够创建一个可以在多种类型而不是单一类型上工作的组件。这允许用户使用这些组件并使用他们自己的类型。

恒等函数是一个函数,它将返回传入的任何内容。你可以将其视为与 echo 命令类似的方式。

如果没有泛型,我们要么必须给标识函数一个特定的类型:

function identity(arg: number): number {
    return arg;
}

或者,我们可以使用 any 类型来描述恒等函数:

function identity(arg: any): any {
    return arg;
}

虽然使用 any 肯定是泛型的,因为它会导致函数接受 arg 类型的任何和所有类型,但实际上我们正在丢失函数返回时该类型的信息。如果我们传入一个数字,我们拥有的唯一信息是可以返回任何类型。

相反,我们需要一种捕获参数类型的方法,以便我们也可以使用它来表示返回的内容。在这里,我们将使用一个类型变量,一种特殊类型的变量,它作用于类型而不是值。

function identity(arg: T): T {
    return arg;
}

我们现在已经向标识函数添加了一个类型变量 Type。这个 Type 允许我们捕获用户提供的类型(例如 number),以便我们以后可以使用该信息。在这里,我们再次使用 Type 作为返回类型。通过检查,我们现在可以看到参数和返回类型使用了相同的类型。这允许我们在函数的一侧和另一侧传输该类型的信息。

我们说这个版本的 identity 函数是泛型的,因为它适用于多种类型。与使用 any 不同,它也与第一个使用数字作为参数和返回类型的 identity 函数一样精确(即,它不会丢失任何信息)。

泛型类型变量

当你开始使用泛型时,你会注意到当你创建像 identity 这样的泛型函数时,编译器会强制你在函数体中正确使用任何泛型类型的参数。也就是说,你实际上将这些参数视为可以是任何和所有类型。

让我们使用之前的 identity 函数:

function identity(arg: T): T {
    return arg;
}

如果我们还想在每次调用时将参数 arg 的长度输出到控制台怎么办?:

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;
}

编译器会抛出一个错误,我们正在使用 arg 的 length 属性,但我们没有申明 arg 有这个属性。请记住,我们之前说过,这些类型变量代表任何和所有类型,因此使用此函数的人可能会传入一个 number 代替,它没有 length 属性。

假设我们实际上打算让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。由于我们使用的是数组,所以 length 属性应该可用。我们可以像创建其他类型的数组一样来描述它:

function identity(arg: T[]): T[] {
    console.log(arg.length);
    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: {<Type>(arg: Type): Type} = identity;

也可用interface代替以上的对象字面量:

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;

泛型约束

你有时可能想要编写一个适用于一组类型的泛型函数,你知道该组类型将具有哪些功能。在我们的 loggingIdentity 示例中,我们希望能够访问 arg 的 length 属性,但编译器无法证明每种类型都有 .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;
}

在泛型约束中使用类型参数

你可以声明受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。并且确保不会意外获取 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<T>(c: {new (): T}): T {
    return new c();
}

实际运用:

class BeeKeeper {
    hasMask: boolean = true;
}

class ZooKeeper {
    nametag: string = "Mikle";
}

class Animal {
    numLegs: number = 4;
}

class Bee extends Animal {
    numLegs = 6;
    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;