泛型
https://juejin.cn/post/6844904184894980104
泛型是什么
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
为了便于大家更好地理解上述的内容,我们来举个例子,在这个例子中,我们将一步步揭示泛型的作用。首先我们来定义一个通用的 identity
函数,该函数接收一个参数并直接返回它:
1 | function identity(value) { |
现在,我们将 identity
函数做适当的调整,以支持 TypeScript 的 Number 类型的参数:
1 | function identity(value: Number): Number { |
这里 identity
的问题是我们将 Number
类型分配给参数和返回类型,使该函数仅可用于该原始类型。但该函数并不是可扩展或通用的,很明显这并不是我们所希望的。
我们确实可以把 Number
换成 any
,我们失去了定义应该返回哪种类型的能力,并且在这个过程中使编译器失去了类型保护的作用。我们的目标是让 identity
函数可以适用于任何特定的类型,为了实现这个目标,我们可以使用泛型来解决这个问题,具体实现方式如下:
1 | function identity<T>(value: T): T { |
对于刚接触 TypeScript 泛型的读者来说,首次看到 `` 语法会感到陌生。但这没什么可担心的,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
参考上面的图片,当我们调用 identity(1)
,Number
类型就像参数 1
一样,它将在出现 T
的任何位置填充该类型。图中 ``内部的T
被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给value
参数用来代替它的类型:此时T
充当的是类型,而不是特定的 Number 类型。
其中 T
代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T
可以用任何有效名称代替。除了 T
之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U
,用于扩展我们定义的 identity
函数:
1 | function identity<T, U>(value: T, message: U): T { |
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
1 | function identity<T, U>(value: T, message: U): T { |
对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。
如你所见,该函数接收你传递给它的任何类型,使得我们可以为不同类型创建可重用的组件。现在我们再来看一下 identity
函数:
1 | function identity<T, U>(value: T, message: U): T { |
相比之前定义的 identity
函数,新的 identity
函数增加了一个类型变量 U
,但该函数的返回类型我们仍然使用 T
。如果我们想要返回两种类型的对象该怎么办呢?针对这个问题,我们有多种方案,其中一种就是使用元组,即为元组设置通用的类型:
1 | function identity<T, U>(value: T, message: U): [T, U] { |
虽然使用元组解决了上述的问题,但有没有其它更好的方案呢?答案是有的,你可以使用泛型接口。
泛型接口
为了解决上面提到的问题,首先让我们创建一个用于的 identity
函数通用 Identities
接口:
1 | interface Identities<V, M> { |
在上述的 Identities
接口中,我们引入了类型变量 V
和 M
,来进一步说明有效的字母都可以用于表示类型变量,之后我们就可以将 Identities
接口作为 identity
函数的返回类型:
1 | function identity<T, U>(value: T, message: U): Identities<T, U> { |
以上代码成功运行后,在控制台会输出以下结果:
1 | 68: number |
泛型除了可以应用在函数和接口之外,它也可以应用在类中,下面我们就来看一下在类中如何使用泛型。