泛型是静态类型语言的基本特征,允许将类型作为参数传递给另一个类型、函数、或者其他结构。TypeScript 支持泛型作为将类型安全引入组件的一种方式。这些组件接受参数和返回值,其类型将是不确定的,直到它在代码中被使用。下面将通过一些示例,介绍如何在函数、类型、类和接口中使用泛型。
1.泛型语法
首先看看TypeScript 泛型的语法。泛型的语法为 ,其中 T 表示传入的类型。在这种情况下,T 和函数参数的工作方式相同,其作为将在创建结构实例时声明的类型的占位符。因此,尖括号内指定的泛型类型也称为泛型类型参数。泛型的定义可以有多个泛型类型做参数,例如:<T, K, Z>。
注意:通常使用单个字母来命名泛型类型。这不是语法规则,我们也可以像 TypeScript 中的任何其他类型一样命名泛型,但这种约定有助于向阅读代码的人传达泛型类型不需要特定类型。
下面通过一个函数的例子来看看泛型的基本语法。假设有一个 JavaScript 函数,它接受两个参数:一个对象和一个包含key的数组。 该函数将基于原始对象返回一个新对象,其仅包含想要的key:
在 pickObjectKeys() 函数中,遍历了keys数组并使用数组中指定的key创建一个新对象。
如果想将这个函数迁移到 TypeScript 以使其类型安全,则可以使用泛型。重构的代码如下:
<T, K extends keyof T> 为函数声明了两个参数类型,其中 K 被分配给了一个类型,该类型是 T 中的 key 的集合。然后将 obj 参数设置为 T,表示任何类型,并将 keys 设置为数组,无论 K 是什么类型。
当传入的 obj 参数为language 对象时,T将 age 设置为number类型,将 extensions 设置为string[]类型,所以变量 ageAndExtensions 的类型为:
这样就会根据提供给 pickObjectKeys 的参数来判断返回值的类型,从而允许函数在知道需要强制执行的特定类型之前灵活地强制执行类型结构。 当在 Visual Studio Code 中使用该函数时,这使得开发体验更好,它将根据提供的对象为 keys 参数提供代码提示的建议。
2.在函数中使用泛型
将泛型与函数一起使用的最常见场景之一就是,当有一些不容易为所有的用例定义类型时,为了使该函数适用于更多情况,就可以使用泛型来定义。下面来看看在函数中使用泛型的常见场景。
(1)分配泛型参数
下面的函数,它返回函数参数传入的内容:
可以为其添加泛型类型以使函数的类型更安全:
这里将函数转化为接受泛型类型参数 T 的泛型函数,它将函数参数的类型,和返回值的类型都设置为 T 。下面来测试一下这个函数:
result 的类型为 123,这是我们传入的数字。此时,TypeScript 使用调用代码本身来推断泛型类型(这是因为TS本身存在着类型推断机制)。这样调用代码不需要传递任何类型参数。
当然,我们也可以显式地将泛型类型参数设置为想要的类型:
这里使用 定义了传入类型,让 TypeScript 标识函数的泛型类型参数 T 为 number 类型。 这将强制number类型作为参数和返回值的类型。当再传入其他类型时,就会报错。
(2)直接传递类型参数
在使用自定义类型时,直接传递类型参数也很有用。
在上面这段代码中,result 为自定义类型 ProgrammingLanguage,它直接传递给了 identity 函数。 如果没有显式地定义类型参数,则result的类型就是 { name: string } 。
另一个常见的例子就是使用函数从 API 获取数据:
这个异步函数将 URL 路径path作为参数,使用 fetch API 向 URL 发出请求,然后返回 JSON 响应值。在这种情况下,fetchApi 函数的返回类型是 Promise,这是 fetch 的响应对象的 json() 调用的返回类型。
将 any 作为返回类型并不会有任何作用,它表示任意类型,使用它将失去静态类型检查。如果我们知道 API 将返回指定结构的对象,则可以使用泛型以使此函数类型更安全:
这里就将函数转换为接受 ResultType 泛型类型参数的泛型函数。 此泛型类型用于函数的返回类型:Promise。
注意:由于这个函数是异步的,因此会返回一个 Promise 对象。 TypeScript 中的 Promise 类型本身是一个泛型类型,它接受 Promise 解析为的值的类型。
可以看到,泛型并没有在参数列表中使用,也没有在TypeScript能够推断其值的其他地方使用。这意味着在调用函数时,必须显式地传递此泛型的类型:
在上面这段代码中,创建了一个名为 User 的新类型,并使用该类型的数组 (User[]) 作为 ResultType 泛型参数的类型。data 变量现在的类型是 User[] 而不是 any。
注意:当使用 await 异步处理函数的结果时,返回类型将是 Promise 中的 T 类型,在这个示例中就是泛型类型 ResultType。
(3)默认类型参数
在上面 fetchApi 函数的例子中,调用代码时必须提供类型参数。 如果调用代码不包含泛型类型参数,则 ResultType 将推断为 unknow。来看下面的例子:
这段代码尝试访问data的a属性,但是由于data是unknow类型,将无法访问对象的属性。
如果不打算为泛型函数的每次调用添加特定的类型,则可以为泛型类型参数添加默认类型。通过在泛型类型参数后面添加 = DefaultType 来完成:
这里不需要在调用 fetchApi 函数时将类型传递给 ResultType 泛型参数,因为它具有默认类型 Record<string, any>。 这意味着 TypeScript 会将data识别为具有string类型的键和any类型值的对象,从而允许访问其属性。
(4)类型参数约束
在某些情况下,泛型类型参数只允许将某些类型传递到泛型中,这时就可以对参数添加约束。
假如有一个存储限制,只能存储所有属性值都为字符串类型的对象。 因此,可以创建一个函数,该函数接受任何对象并返回另一个对象,其 key 值与原始对象相同,但所有值都转换为字符串。代码如下:
在这段代码中,stringifyObjectKeyValues 函数使用 reduce 数组方法遍历包含原始对象的key的数组,将属性值字符串化并将它们添加到新数组中。
为确保调用代码始终传入一个对象作为参数,可以在泛型类型 T 上使用类型约束:
extends Record<string, any> 被称为泛型类型约束,它允许指定泛型类型必须可分配给 extends 关键字之后的类型。 在这种情况下,Record<string, any> 表示具有string类型的键和any类型的值的对象。 我们可以使类型参数扩展任何有效的 TypeScript 类型。
在调用reduce时,reducer函数的返回类型是基于累加器的初始值。 {} as { [K in keyof T]: string } 通过对空对象 {} 使用类型断言将累加器的初始值的类型设置为{ [K in keyof T]: string }。 type { [K in keyof T]: string } 创建了一个新类型,其键与 T 相同,但所有值都设置为字符串类型,这称为映射类型。
3. 在接口、类和类型中使用泛型
在 TypeScript 中创建接口和类时,使用泛型类型参数来设置结果对象的类型会很有用。 例如,一个类可能具有不同类型的属性,具体取决于传入构造函数的内容。 下面就来看看在类和接口中声明泛型类型参数的语法。
(1)接口和类中的泛型
要创建泛型接口,可以在接口名称后添加类型参数列表:
这里声明了一个具有field字段的接口,field字段的类型由传入 T 的类型确定。
对于类,它的语法和接口定义几乎是相同的:
通用接口/类的一个常见例子就是当有一个类型取决于如何使用接口/类的字段。 假设有一个 HttpApplication 类,用于处理对 API 的 HTTP 请求,并且某些 context 值将被传递给每个请求处理程序。 代码如下:
这个类储存了一个 context,它的类型作为 get 方法中handler函数的参数类型传入。 在使用时,传递给 get 方法的handler的参数类型将从传递给类构造函数的内容中推断出来:
在这段代码中,TypeScript 会将 context.someValue 的类型推断为 boolean。
(2)自定义类型中的泛型
将泛型应用于类型的语法类似于它们应用于接口和类的方式。 来看下面的代码:
这个泛型类型返回类型参数传递的类型。使用以下代码来实现这种类型:
在这种情况下,B 的类型就是number。
泛型类型通常用于创建工具类型,尤其是在使用映射类型时。 TypeScript 内置了许多工具类型。 例如 Partial实用工具类型,它传入类型 T 并返回另一种具有与 T 相同的类型,但它们的所有字段都设置为可选。 Partial的实现如下:
这里,Partial 接受一个类型,遍历它的属性类型,然后将它们作为可选的新类型返回。
注意:由于 Partial 已经内置到了 TypeScript 中,因此将此代码编译到 TypeScript 环境中会重新声明 Partial 并引发错误。 上面的 Partial 实现例子只是为了说明在自定义类型中的泛型是如何使用的。