TypeScript项目开发实战
上QQ阅读APP看书,第一时间看更新

1.3.9 使用泛型,将相同的代码用于不同的类型

刚开始在TypeScript中开发类的时候,我们很容易重复地编写代码,每次只是改变依赖的类型。例如,如果想存储一个整数队列,可能要编写下面的类:

调用这段代码很简单:

然后,我们决定还需要创建一个字符串队列,所以也添加了相应的代码:

很容易看到,这样的代码添加得越多,我们的工作就变得越乏味,越容易出错。假设我们在其中一个实现中忘了添加移出(shift)操作。移出操作允许我们取出并返回数组中的第一个元素,从而得到队列的核心行为——先进先出(First In First Out,FIFO)。如果我们忘记移出操作,则实现的会是一个栈。这可能导致代码中出现难以察觉而又危险的bug。

TypeScript提供了创建泛型的能力。泛型是一种类型,通过占位符来代表要使用的类型。具体使用什么类型,要由调用该泛型的代码决定。泛型包含在< >内,出现在类名、方法名等的后面。如果我们使用泛型重写前面的队列,就能够理解这段话的意思:

我们分解一下这段代码:

这里创建了一个名为Queue的类,它接受任何类型。<T>语法告诉TypeScript,这个类中任何地方出现的T都指代传入的类型:

下面的代码中第一次出现泛型。编译器不会将数组固定为特定的类型,而是会使用泛型创建该数组:

我们将代码中的具体类型替换为泛型。注意,在Pop方法中,TypeScript允许同时使用泛型和undefined关键字。

代码的使用方法将发生变化。我们现在只需将想要应用的类型告诉Queue对象:

有一点特别有帮助:当我们为泛型指定类型后,TypeScript会限制其不能改变。因此,在上面的代码中,如果我们试图在queue变量中添加一个字符串,TypeScript将无法编译代码。

虽然TypeScript竭尽所能来保护我们,但是我们需要记住,TypeScript会转换成为JavaScript,这意味着它无法保护代码不被滥用。虽然TypeScript会保护我们分配的类型,但是如果编写的外部JavaScript也会调用泛型,就无法阻止外部代码添加不受支持的值。只有在编译时才会强制实施泛型的类型,所以如果调用代码有可能不在我们的控制范围内,就应该在代码中针对不兼容类型添加一些防护措施。

在泛型列表中,并不是只能有一种类型。只要类型具有不同的名称,泛型就允许在定义中指定任意数量的类型,如下所示:

观察力敏锐的读者会注意到,我们前面已经遇到过泛型。在创建混入的时候,我们在Constructor类型中使用了泛型。

如果我们想从泛型调用某个特定的方法,该怎么办?因为TypeScript需要知道类型的底层实现,所以会严格限制我们能做什么。这意味着不能使用下面的代码:

因为TypeScript无法猜测到我们想要在这里使用IStream接口,所以在编译这段代码时会报错。好在通过使用泛型约束,可以告诉泛型在这里使用特定的类型:

<T extends IStream>告诉TypeScript,我们将使用任何基于IStream接口的类。

虽然我们可以将泛型约束为类型,但是一般来说,要把泛型约束为接口。这样一来,我们在约束中能够使用的类就灵活多了,而且不会限制我们只能使用从特定基类继承而来的类。

为了展示其用法,创建两个实现了IStream接口的类:

把它们用作泛型Data实现的类型约束:

这就告诉webStream和diskStream,它们能够访问我们的类。要使用它们,仍然需要传入一个实例,如下所示:

虽然这里是在类级别声明泛型及其约束,但这并不是必需的。如果有需要,我们可以在方法级别声明更细粒度的泛型。在这里,如果我们想要在代码中的多个位置引用泛型类型,那么在类级别声明泛型是合理的。如果我们只需要在一两个方法中应用特定的泛型,那么可以将类签名修改为如下所示: