如何在 TypeScript 中使用泛型

程序员成长指北

共 19994字,需浏览 40分钟

 ·

2023-03-11 10:50

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

英文 | https://www.digitalocean.com/community/tutorials/how-to-use-generics-in-typescript

介绍
泛型是静态类型语言的基本特征,允许开发人员将类型作为参数传递给另一种类型、函数或其他结构。当开发人员使他们的组件成为通用组件时,他们使该组件能够接受和强制在使用组件时传入的类型,这提高了代码灵活性,使组件可重用并消除重复。
TypeScript 完全支持泛型,以此将类型安全性引入到接受参数和返回值的组件中,这些参数和返回值的类型,在稍后的代码中使用之前是不确定的。在今天的内容中,我们将尝试 TypeScript 泛型的真实示例,并探索它们如何在函数、类型、类和接口中使用。
我们还将使用泛型创建映射类型和条件类型,这将帮助我们创建可以灵活应用于代码中所有必要情况的 TypeScript 组件。
准备工作

介绍
TypeScript 是 JavaScript 语言的扩展,它使用 JavaScript 运行时和编译时类型检查器。
TypeScript 提供了多种方法来表示代码中的对象,其中一种是使用接口。 TypeScript 中的接口有两种使用场景:您可以创建类必须遵循的约定,例如,这些类必须实现的成员,还可以在应用程序中表示类型,就像普通的类型声明一样。 
您可能会注意到接口和类型共享一组相似的功能。
事实上,一个几乎总是可以替代另一个。
主要区别在于接口可能对同一个接口有多个声明,TypeScript 将合并这些声明,而类型只能声明一次。您还可以使用类型来创建原始类型(例如字符串和布尔值)的别名,这是接口无法做到的。
TypeScript 中的接口是表示类型结构的强大方法。它们允许您以类型安全的方式使用这些结构并同时记录它们,从而直接改善开发人员体验。
在今天的文章中,我们将在 TypeScript 中创建接口,学习如何使用它们,并了解普通类型和接口之间的区别。
我们将尝试不同的代码示例,可以在 TypeScript 环境或 TypeScript Playground(一个允许您直接在浏览器中编写 TypeScript 的在线环境)中遵循这些示例。
准备工作
要完成今天的示例,我们将需要做如下准备工作:
  • 一个环境。我们可以执行 TypeScript 程序以跟随示例。要在本地计算机上进行设置,我们将需要准备以下内容。

  • 为了运行处理 TypeScript 相关包的开发环境,同时安装了 Node 和 npm(或 yarn)。本文教程中使用 Node.js 版本 为14.3.0 和 npm 版本 6.14.5 进行了测试。要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。如果您使用的是适用于 Linux 的 Windows 子系统 (WSL),这也适用。

  • 此外,我们需要在机器上安装 TypeScript 编译器 (tsc)。为此,请参阅官方 TypeScript 网站。

  • 如果你不想在本地机器上创建 TypeScript 环境,你可以使用官方的 TypeScript Playground 来跟随。

  • 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如解构、rest 运算符和导入/导出。如果您需要有关这些主题的更多信息,建议阅读我们的如何用 JavaScript 编写代码系列。

  • 本文教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。你也可以在 TypeScript Playground 中尝试这些好处。

本教程中显示的所有示例都是使用 TypeScript 4.2.3 版创建的。
泛型语法
在进入泛型应用之前,本教程将首先介绍 TypeScript 泛型的语法,然后通过一个示例来说明它们的一般用途。
泛型出现在尖括号内的 TypeScript 代码中,格式为 <T>,其中 T 表示传入的类型。<T> 可以理解为 T 类型的泛型。
在这种情况下,T 将以与函数中参数相同的方式运行,作为将在创建结构实例时声明的类型的占位符。因此,尖括号内指定的泛型类型也称为泛型类型参数或只是类型参数。多个泛型类型也可以出现在单个定义中,例如 <T, K, A>。
注意:按照惯例,程序员通常使用单个字母来命名泛型类型。这不是语法规则,你可以像 TypeScript 中的任何其他类型一样命名泛型,但这种约定有助于立即向那些阅读你的代码的人传达泛型类型不需要特定类型。
泛型可以出现在函数、类型、类和接口中。本教程稍后将介绍这些结构中的每一个,但现在将使用一个函数作为示例来说明泛型的基本语法。
要了解泛型有多么有用,假设您有一个 JavaScript 函数,它接受两个参数:一个对象和一个键数组。该函数将基于原始对象返回一个新对象,但仅包含您想要的键:

function pickObjectKeys(obj, keys) {  let result = {}  for (const key of keys) {    if (key in obj) {      result[key] = obj[key]    }  }  return result}

此代码段显示了 pickObjectKeys() 函数,该函数遍历keys数组并使用数组中指定的键创建一个新对象。

下面是一个展示如何使用该函数的示例:

const language = {  name: "TypeScript",  age: 8,  extensions: ['ts', 'tsx']}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])

这声明了一种对象,然后使用 pickObjectKeys() 函数隔离 age 和 extensions 属性。 ageAndExtensions 的值如下:

{  age: 8,  extensions: ['ts', 'tsx']}

如果要将此代码迁移到 TypeScript 以使其类型安全,则必须使用泛型。 我们可以通过添加以下突出显示的行来重构代码:

function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {  let result = {} as Pick<T, K>  for (const key of keys) {    if (key in obj) {      result[key] = obj[key]    }  }  return result}
const language = { name: "TypeScript", age: 8, extensions: ['ts', 'tsx']}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])

<T, K extends keyof T> 为函数声明了两个参数类型,其中 K 被分配一个类型,该类型是 T 中的key的并集。

然后将 obj 函数参数设置为 T 表示的任何类型,并将key设置为数组, 无论 K 代表什么类型。 

由于在语言对象的情况下 T 将 age 设置为数字并将 extensions 设置为字符串数组,因此,变量 ageAndExtensions 现在将被分配具有属性 age: number 和 extensions: string[] 的对象的类型。

这会根据提供给 pickObjectKeys 的参数强制执行返回类型,从而允许函数在知道需要强制执行的特定类型之前灵活地强制执行类型结构。 

当在 Visual Studio Code 等 IDE 中使用该函数时,这也增加了更好的开发人员体验,它将根据您提供的对象为 keys 参数创建建议。 这显示在以下屏幕截图中:

了解如何在 TypeScript 中创建泛型后,您现在可以继续探索在特定情况下使用泛型。 本教程将首先介绍如何在函数中使用泛型。

将泛型与函数一起使用

将泛型与函数一起使用的最常见场景之一是当您有一些代码不容易为所有用例键入时。 为了使该功能适用于更多情况,您可以包括泛型类型。 

在此步骤中,您将运行一个恒等函数示例来说明这一点。 您还将探索一个异步示例,了解何时将类型参数直接传递给您的泛型,以及如何为您的泛型类型参数创建约束和默认值。

分配通用参数

看一下下面的函数,它返回作为第一个参数传入的内容:

function identity(value) {  return value;}
您可以添加以下代码以使函数在 TypeScript 中类型安全:
function identity<T>(value: T): T{  return value;}

你把你的函数变成了一个泛型函数,它接受泛型类型参数 T,这是第一个参数的类型,然后将返回类型设置为与 : T 相同。

接下来,添加以下代码来试用该功能:

function identity<T>(value: T): T {  return value;}
const result = identity(123);

结果的类型为 123,这是您传入的确切数字。这里的 TypeScript 从调用代码本身推断泛型类型。 这样调用代码不需要传递任何类型参数。 您也可以显式地将泛型类型参数设置为您想要的类型:

function identity<T>(value: T): T {  return value;}
const result = identity<number>(123);

在此代码中,result 具有类型编号。 通过使用 <number> 代码传入类型,您明确地让 TypeScript 知道您希望身份函数的泛型类型参数 T 的类型为 number。 这将强制将数字类型作为参数和返回值。

直接传递类型参数

直接传递类型参数在使用自定义类型时也很有用。 例如,看看下面的代码:

type ProgrammingLanguage = {  name: string;};
function identity<T>(value: T): T { return value;}
const result = identity<ProgrammingLanguage>({ name: "TypeScript" });

在此代码中,result 具有自定义类型 ProgrammingLanguage,因为它直接传递给标识函数。 如果您没有明确包含类型参数,则结果将具有类型 { name: string } 。

使用 JavaScript 时的另一个常见示例是使用包装函数从 API 检索数据:

async function fetchApi(path: string) {  const response = await fetch(`https://example.com/api${path}`)  return response.json();}

此异步函数将 URL 路径作为参数,使用 fetch API 向 URL 发出请求,然后返回 JSON 响应值。 在这种情况下,fetchApi 函数的返回类型将是 Promise<any>,这是对 fetch 的响应对象调用 json() 的返回类型。

将 any 作为返回类型并不是很有帮助。 any 表示任何 JavaScript 值,使用它你将失去静态类型检查,这是 TypeScript 的主要优点之一。 如果您知道 API 将返回给定形状的对象,则可以使用泛型使此函数类型安全:

async function fetchApi<ResultType>(path: string): Promise<ResultType>{  const response = await fetch(`https://example.com/api${path}`);  return response.json();}

突出显示的代码将您的函数转换为接受 ResultType 泛型类型参数的泛型函数。 此泛型类型用于函数的返回类型:Promise<ResultType>。

注意:由于您的函数是异步的,因此,您必须返回一个 Promise 对象。 TypeScript Promise 类型本身是一种通用类型,它接受 promise 解析为的值的类型。

如果仔细查看您的函数,您会发现参数列表或 TypeScript 能够推断其值的任何其他地方都没有使用泛型。 这意味着调用代码在调用您的函数时必须显式传递此泛型的类型。

以下是检索用户数据的 fetchApi 通用函数的可能实现:

type User = {  name: string;}
async function fetchApi<ResultType>(path: string): Promise<ResultType> { const response = await fetch(`https://example.com/api${path}`); return response.json();}
const data = await fetchApi<User[]>('/users')
export {}

在此代码中,您将创建一个名为 User 的新类型,并使用该类型的数组 (User[]) 作为 ResultType 泛型参数的类型。 数据变量现在具有类型 User[] 而不是任何。

注意:当您使用 await 异步处理函数的结果时,返回类型将是 Promise<T> 中 T 的类型,在本例中是通用类型 ResultType。

默认类型参数

像您一样创建通用的 fetchApi 函数,调用代码始终必须提供类型参数。 如果调用代码不包含泛型类型,则 ResultType 将绑定为未知。 以下面的实现为例:

async function fetchApi<ResultType>(path: string): Promise<ResultType> {  const response = await fetch(`https://example.com/api${path}`);  return response.json();}
const data = await fetchApi('/users')
console.log(data.a)
export {}

此代码尝试访问数据的理论上的属性。 但由于数据类型未知,这段代码将无法访问对象的属性。

如果您不打算将特定类型添加到泛型函数的每次调用中,则可以将默认类型添加到泛型类型参数中。 这可以通过在泛型类型之后添加 = DefaultType 来完成,如下所示:

async function fetchApi<ResultType= Record<string, any>>(path: string): Promise<ResultType> {  const response = await fetch(`https://example.com/api${path}`);  return response.json();}
const data = await fetchApi('/users')
console.log(data.a)
export {}

使用此代码,您不再需要在调用 fetchApi 函数时将类型传递给 ResultType 泛型参数,因为它具有默认类型 Record<string, any>。 这意味着 TypeScript 会将数据识别为具有字符串类型的键和任意类型的值的对象,从而允许您访问其属性。

类型参数约束

在某些情况下,泛型类型参数需要只允许将某些形状传递给泛型。 要为您的泛型创建额外的特殊层,您可以对您的参数施加约束。

假设您有一个存储限制,您只能存储所有属性都具有字符串值的对象。 为此,您可以创建一个函数,它接受任何对象并返回另一个对象,该对象具有与原始对象相同的键,但所有值都转换为字符串。 这个函数将被称为 stringifyObjectKeyValues。

这个函数将是一个通用函数。 这样,您就可以使生成的对象具有与原始对象相同的形状。 该函数将如下所示:

function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {  return Object.keys(obj).reduce((acc, key) =>  ({    ...acc,    [key]: JSON.stringify(obj[key])  }), {} as { [K in keyof T]: string })}

在此代码中,stringifyObjectKeyValues 使用 reduce 数组方法迭代原始键数组,将值字符串化并将它们添加到新数组中。

为确保调用代码始终将对象传递给您的函数,您在泛型类型 T 上使用类型约束,如以下突出显示的代码所示:

function stringifyObjectKeyValues<Textends Record<string, any>>(obj: T) {  // ...}

extends Record<string, any> 被称为泛型类型约束,它允许您指定您的泛型类型必须可分配给 extends 关键字之后的类型。 

在这种情况下,Record<string, any> 表示一个具有字符串类型的键和任意类型的值的对象。 您可以让您的类型参数扩展任何有效的 TypeScript 类型。

在调用 reduce 时,reducer 函数的返回类型基于累加器的初始值。 {} as { [K in keyof T]: string } 代码通过对空对象 {} 进行类型转换,将累加器初始值的类型设置为 { [K in keyof T]: string }。 

type { [K in keyof T]: string } 创建一个新类型,它具有与 T 相同的键,但所有值都设置为字符串类型,这称为映射类型,本教程将在后面的部分中进一步探讨。

以下代码显示了 stringifyObjectKeyValues 函数的实现:

function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {  return Object.keys(obj).reduce((acc, key) =>  ({    ...acc,    [key]: JSON.stringify(obj[key])  }), {} as { [K in keyof T]: string })}
const stringifiedValues = stringifyObjectKeyValues({ a: "1", b: 2, c: true, d: [1, 2, 3]})

变量 stringifiedValues 将具有以下类型:

{  a: string;  b: string;  c: string;  d: string;}

这将确保返回值与函数的目的一致。

本节介绍了将泛型与函数一起使用的多种方法,包括直接分配类型参数以及为参数形状设置默认值和约束。 

接下来,您将通过一些示例来了解泛型如何使接口和类适用于更多情况。

将泛型与接口、类和类型一起使用

在 TypeScript 中创建接口和类时,使用泛型类型参数来设置结果对象的形状会很有用。 

例如,一个类可能具有不同类型的属性,具体取决于传递给构造函数的内容。 在本节中,您将了解在类和接口中声明泛型类型参数的语法,并检查 HTTP 应用程序中的常见用例。

通用接口和类

要创建通用接口,您可以在接口名称之后添加类型参数列表:

interface MyInterface<T> {  field: T}

这声明了一个接口,该接口具有一个属性字段,其类型由传递给 T 的类型确定。

对于类,语法几乎相同:

class MyClass<T> {  field: T  constructor(field: T) {    this.field = field  }}

通用接口/类的一个常见用例是当您有一个字段,其类型取决于客户端代码如何使用接口/类时。 

假设您有一个 HttpApplication 类,用于处理对 API 的 HTTP 请求,并且某些上下文值将传递给每个请求处理程序。 这样做的一种方法是:

class HttpApplication<Context> {  context: Context  constructor(context: Context) {    this.context = context;  }
// ... implementation
get(url: string, handler: (context: Context) => Promise<void>): this { // ... implementation return this; }}

此类存储一个上下文,其类型作为 get 方法中处理函数的参数类型传入。 在使用过程中,传递给 get 处理程序的参数类型将从传递给类构造函数的内容中正确推断出来。

...const context = { someValue: true };const app = new HttpApplication(context);
app.get('/api', async () => { console.log(context.someValue)});

在此实现中,TypeScript 会将 context.someValue 的类型推断为布尔值。

通用类型

现在已经了解了类和接口中泛型的一些示例,您现在可以继续创建泛型自定义类型。 将泛型应用于类型的语法类似于将泛型应用于接口和类的语法。 看看下面的代码:

type MyIdentityType<T> = T

此泛型类型返回作为类型参数传递的类型。 假设您使用以下代码实现了这种类型:

...type B = MyIdentityType<number>

在这种情况下,类型 B 将是类型 number。

通用类型通常用于创建辅助类型,尤其是在使用映射类型时。 TypeScript 提供了许多预构建的帮助程序类型。 

一个这样的例子是 Partial 类型,它采用类型 T 并返回另一个与 T 具有相同形状的类型,但它们的所有字段都设置为可选。 Partial 的实现如下所示:

type Partial<T> = {  [P in keyof T]?: T[P];};

这里的 Partial 类型接受一个类型,遍历其属性类型,然后将它们作为可选类型返回到新类型中。

注意:由于 Partial 已经内置到 TypeScript 中,因此将此代码编译到您的 TypeScript 环境中会重新声明 Partial 并引发错误。 这里引用的Partial的实现只是为了说明。

要了解泛型类型有多么强大,假设您有一个对象字面量,用于存储从一家商店到您的业务分销网络中所有其他商店的运输成本。 每个商店将由一个三字符代码标识,如下所示:

{  ABC: {    ABC: null,    DEF: 12,    GHI: 13,  },  DEF: {    ABC: 12,    DEF: null,    GHI: 17,  },  GHI: {    ABC: 13,    DEF: 17,    GHI: null,  },}

该对象是表示商店位置的对象的集合。 在每个商店位置中,都有表示运送到其他商店的成本的属性。 例如,从 ABC 运往 DEF 的成本是 12。从一家商店到它自己的运费为空,因为根本没有运费。

为确保其他商店的位置具有一致的值,并且商店运送到自身的始终为空,您可以创建一个通用的帮助器类型:

type IfSameKeyThanParentTOtherwiseOtherType<Keys extends string, T, OtherType> = {  [K in Keys]: {    [SameThanK in K]: T;  } &    { [OtherThanK in Exclude<Keys, K>]: OtherType };};

IfSameKeyThanParentTOtherwiseOtherType 类型接收三个通用类型。 第一个,Keys,是你想要确保你的对象拥有的所有键。 在这种情况下,它是所有商店代码的联合。 

T 是当嵌套对象字段具有与父对象上的键相同的键时的类型,在这种情况下,它表示运送到自身的商店位置。 最后,OtherType 是 key 不同时的类型,表示一个商店发货到另一个商店。

你可以像这样使用它:

...type Code = 'ABC' | 'DEF' | 'GHI'
const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = { ABC: { ABC: null, DEF: 12, GHI: 13, }, DEF: { ABC: 12, DEF: null, GHI: 17, }, GHI: { ABC: 13, DEF: 17, GHI: null, },}

此代码现在强制执行类型形状。 如果您将任何键设置为无效值,TypeScript 将报错:

...const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {  ABC: {    ABC: 12,    DEF: 12,    GHI: 13,  },  DEF: {    ABC: 12,    DEF: null,    GHI: 17,  },  GHI: {    ABC: 13,    DEF: 17,    GHI: null,  },}

由于 ABC 与自身之间的运费不再为空,TypeScript 将抛出以下错误:

OutputType 'number' is not assignable to type 'null'.(2322)

您现在已经尝试在接口、类和自定义帮助程序类型中使用泛型。 接下来,您将进一步探讨本教程中已经多次出现的主题:使用泛型创建映射类型。

使用泛型创建映射类型

在使用 TypeScript 时,有时您需要创建一个与另一种类型具有相同形状的类型。 这意味着它应该具有相同的属性,但属性的类型设置为不同的东西。 对于这种情况,使用映射类型可以重用初始类型形状并减少应用程序中的重复代码。

在 TypeScript 中,这种结构被称为映射类型并依赖于泛型。 在本节中,您将看到如何创建映射类型。

想象一下,您想要创建一个类型,给定另一个类型,该类型返回一个新类型,其中所有属性都设置为具有布尔值。 您可以使用以下代码创建此类型:

type BooleanFields<T> = {  [K in keyof T]: boolean;}

在这种类型中,您使用语法 [K in keyof T] 来指定新类型将具有的属性。 keyof T 运算符用于返回具有 T 中所有可用属性名称的联合。然后使用 K in 语法指定新类型的属性是返回的联合类型中当前可用的所有属性 T键。

这将创建一个名为 K 的新类型,它绑定到当前属性的名称。 这可用于使用语法 T[K] 访问原始类型中此属性的类型。 在这种情况下,您将属性的类型设置为布尔值。

此 BooleanFields 类型的一个使用场景是创建一个选项对象。 假设您有一个数据库模型,例如用户。 

从数据库中获取此模型的记录时,您还将允许传递一个指定要返回哪些字段的对象。 

该对象将具有与模型相同的属性,但类型设置为布尔值。 在一个字段中传递 true 意味着您希望它被返回,而 false 则意味着您希望它被省略。

您可以在现有模型类型上使用 BooleanFields 泛型来返回与模型具有相同形状的新类型,但所有字段都设置为布尔类型,如以下突出显示的代码所示:

type BooleanFields<T> = {  [K in keyof T]: boolean;};
type User = { email: string; name: string;}
type UserFetchOptions = BooleanFields<User>;

在此示例中,UserFetchOptions 将与这样创建它相同:

type UserFetchOptions = {  email: boolean;  name: boolean;}

创建映射类型时,您还可以为字段提供修饰符。 一个这样的例子是 TypeScript 中可用的现有泛型类型,称为 Readonly<T>。 Readonly<T> 类型返回一个新类型,其中传递类型的所有属性都设置为只读属性。 这种类型的实现如下所示:

type Readonly<T> = {  readonly [K in keyof T]: T[K]}

注意:由于 Readonly 已经内置到 TypeScript 中,因此将此代码编译到您的 TypeScript 环境中会重新声明 Readonly 并引发错误。 这里引用的Readonly的实现只是为了说明的目的。

请注意修饰符 readonly,它作为前缀添加到此代码中的 [K in keyof T] 部分。 

目前,可以在映射类型中使用的两个可用修饰符是 readonly 修饰符,它必须作为前缀添加到属性,以及 ? 修饰符,可以作为属性的后缀添加。 这 ? 修饰符将字段标记为可选。 

两个修饰符都可以接收一个特殊的前缀来指定是否应该删除修饰符 (-) 或添加 (+)。 如果仅提供修饰符,则假定为 +。

现在您可以使用映射类型基于您已经创建的类型形状创建新类型,您可以继续讨论泛型的最终用例:条件类型。

使用泛型创建条件类型

在本节中,您将尝试 TypeScript 中泛型的另一个有用功能:创建条件类型。 首先,您将了解条件类型的基本结构。 然后,您将通过创建一个条件类型来探索高级用例,该条件类型省略基于点表示法的对象类型的嵌套字段。

条件类型的基本结构

条件类型是根据某些条件具有不同结果类型的泛型类型。 例如,看看下面的泛型类型 IsStringType<T>:

type IsStringType<T> = T extends string ? true : false;

在此代码中,您正在创建一个名为 IsStringType 的新泛型类型,它接收单个类型参数 T。在您的类型定义中,您使用的语法看起来像使用 JavaScript 中的三元运算符的条件表达式:T extends string ? 真假。 

此条件表达式正在检查类型 T 是否扩展了类型字符串。 如果是,则结果类型将是完全正确的类型; 否则,它将被设置为 false 类型。

注意:此条件表达式是在编译期间求值的。 TypeScript 仅适用于类型,因此请确保始终将类型声明中的标识符读取为类型,而不是值。 在此代码中,您使用每个布尔值的确切类型,true 和 false。

要尝试这种条件类型,请将一些类型作为其类型参数传递:

type IsStringType<T> = T extends string ? true : false;
type A = "abc";type B = { name: string;};
type ResultA = IsStringType<A>;type ResultB = IsStringType<B>;

在此代码中,您创建了两种类型,A 和 B。类型 A 是字符串文字“abc”的类型,而类型 B 是具有名为 name of type string 属性的对象的类型。 

然后将这两种类型与 IsStringType 条件类型一起使用,并将结果类型存储到两个新类型 ResultA 和 ResultB 中。

如果检查 ResultA 和 ResultB 的结果类型,您会注意到 ResultA 类型设置为准确的类型 true,而 ResultB 类型设置为 false。 这是正确的,因为 A 确实扩展了字符串类型而 B 没有扩展字符串类型,因为它被设置为具有字符串类型的单个名称属性的对象的类型。

条件类型的一个有用特性是它允许您使用特殊关键字 infer 在 extends 子句中推断类型信息。 然后可以在条件的真实分支中使用这种新类型。 此功能的一种可能用法是检索任何函数类型的返回类型。

编写以下 GetReturnType 类型来说明这一点:

type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;

在此代码中,您将创建一个新的泛型类型,它是一个名为 GetReturnType 的条件类型。 此泛型类型接受单个类型参数 T。

在类型声明本身内部,您正在检查类型 T 是否扩展了与函数签名匹配的类型,该函数签名接受可变数量的参数(包括零),然后您推断返回 该函数的类型创建一个新类型 U,可在条件的真实分支内使用。 

U 的类型将绑定到传递函数的返回值的类型。 如果传递的类型 T 不是函数,则代码将返回 never 类型。

使用您的类型和以下代码:

type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
function someFunction() { return true;}
type ReturnTypeOfSomeFunction = GetReturnType<typeof someFunction>;

在此代码中,您将创建一个名为 someFunction 的函数,该函数返回 true。 然后使用 typeof 运算符将此函数的类型传递给 GetReturnType 泛型,并将结果类型存储在 ReturnTypeOfSomeFunction 类型中。

由于 someFunction 变量的类型是函数,因此条件类型将评估条件的真实分支。 这将返回类型 U 作为结果。 

类型 U 是从函数的返回类型推断出来的,在本例中是布尔值。 如果检查 ReturnTypeOfSomeFunction 的类型,您会发现它已正确设置为布尔类型。

高级条件类型用例

条件类型是 TypeScript 中可用的最灵活的功能之一,允许创建一些高级实用程序类型。 

在本节中,您将通过创建一个名为 NestedOmit<T, KeysToOmit> 的条件类型来探索这些用例之一。 

此实用程序类型将能够省略对象中的字段,就像现有的 Omit<T, KeysToOmit> 实用程序类型一样,但也允许使用点表示法省略嵌套字段。

使用新的 NestedOmit<T, KeysToOmit> 泛型,您将能够使用以下示例中所示的类型:

type SomeType = {  a: {    b: string,    c: {      d: number;      e: string[]    },    f: number  }  g: number | string,  h: {    i: string,    j: number,  },  k: {    l: number,<F3>  }}
type Result = NestedOmit<SomeType, "a.b" | "a.c.e" | "h.i" | "k">;

此代码声明了一个名为 SomeType 的类型,它具有嵌套属性的多级结构。 使用 NestedOmit 泛型,传入类型,然后列出要省略的属性的键。 

请注意如何在第二个类型参数中使用点符号来标识要省略的键。 然后将结果类型存储在 Result 中。

构造此条件类型将使用 TypeScript 中可用的许多功能,例如,模板文字类型、泛型、条件类型和映射类型。

要尝试这个泛型,首先创建一个名为 NestedOmit 的泛型类型,它接受两个类型参数:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>

第一个类型参数称为 T,它必须是可分配给 Record<string, any> 类型的类型。 这将是您要从中省略属性的对象的类型。 

第二个类型参数叫做KeysToOmit,必须是字符串类型。 您将使用它来指定要从类型 T 中省略的键。

接下来,通过添加以下突出显示的代码来检查 KeysToOmit 是否可分配给 ${infer KeyPart1}.${infer KeyPart2} 类型:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>=  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`

在这里,您使用模板文字字符串类型,同时,利用条件类型推断模板文字本身内部的其他两种类型。 

通过推断模板文字字符串类型的两个部分,您将字符串拆分为另外两个字符串。 第一部分将分配给 KeyPart1 类型,并将包含第一个点之前的所有内容。 

第二部分将分配给 KeyPart2 类型,并将包含第一个点之后的所有内容。 如果您将“a.b.c”作为 KeysToOmit 传递,则最初 KeyPart1 将设置为确切的字符串类型“a”,而 KeyPart2 将设置为“b.c”。

接下来,您将添加三元运算符来定义条件的第一个真分支:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`    ?      KeyPart1 extends keyof T

这使用 KeyPart1 extends keyof T 来检查 KeyPart1 是否是给定类型 T 的有效属性。如果您确实有一个有效的键,请添加以下代码以使条件计算为两种类型之间的交集:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`    ?      KeyPart1 extends keyof T      ?        Omit<T, KeyPart1>        & {          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>        }

Omit<T, KeyPart1> 是一种使用 TypeScript 默认附带的 Omit 助手构建的类型。 此时,KeyPart1 不是点表示法:它将包含一个字段的确切名称,该字段包含您希望从原始类型中省略的嵌套字段。 因此,您可以安全地使用现有的实用程序类型。

您正在使用 Omit 删除 T[KeyPart1] 中的一些嵌套字段,为此,您必须重建 T[KeyPart1] 的类型。 

为避免重建整个 T 类型,您使用 Omit 仅从 T 中删除 KeyPart1,同时保留其他字段。 然后,您将在下一部分的类型中重建 T[KeyPart1]。

[KeyPart1 中的新键]:NestedOmit<T[NewKeys], KeyPart2> 是一个映射类型,其中属性是可分配给 KeyPart1 的属性,这意味着您刚刚从 KeysToOmit 中提取的部分。 

这是您要删除的字段的父项。 如果您通过了 a.b.c,在第一次评估您的条件时,它将是“a”中的 NewKeys。 

然后将此属性的类型设置为递归调用 NestedOmit 实用程序类型的结果,但现在使用 T[NewKeys] 将此属性的类型作为第一个类型参数传递给 T,并作为第二个类型参数传递其余键以点表示法表示,在 KeyPart2 中可用。

在内部条件的 false 分支中,返回绑定到 T 的当前类型,就好像 KeyPart1 不是 T 的有效键一样:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`    ?      KeyPart1 extends keyof T      ?        Omit<T, KeyPart1>        & {          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>        }      : T

条件的这个分支意味着你试图省略一个 T 中不存在的字段。在这种情况下,没有必要再进一步了。

最后,在外部条件的 false 分支中,使用现有的 Omit 实用程序类型从 Type 中省略 KeysToOmit:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`    ?      KeyPart1 extends keyof T      ?        Omit<T, KeyPart1>        & {          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>        }      : T    : Omit<T, KeysToOmit>;

如果条件 KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}` 为假,这意味着 KeysToOmit 没有使用点符号,因此,您可以使用现有的 Omit 实用程序类型。

现在,要使用新的 NestedOmit 条件类型,请创建一个名为 NestedObject 的新类型:

type NestedObject = {  a: {    b: {      c: number;      d: number;    };    e: number;  };  f: number;};

然后对其调用 NestedOmit 以省略 a.b.c 处可用的嵌套字段:

type Result = NestedOmit<NestedObject, "a.b.c">;

在第一次评估条件类型时,外部条件将为真,因为字符串文字类型“a.b.c”可分配给模板文字类型“${infer KeyPart1}.${infer KeyPart2}”。 

在这种情况下,KeyPart1 将被推断为字符串文字类型“a”,而 KeyPart2 将被推断为字符串的剩余部分,在本例中为“b.c”。

现在将评估内部条件。 这将评估为真,因为此时 KeyPart1 是 T 的键。KeyPart1 现在是“a”,而 T 确实有一个属性“a”:

type NestedObject = {a: {b: {c: number;d: number;};e: number;};  f: number;};

继续评估条件,您现在位于内部 true 分支内。 这将构建一个新类型,它是其他两种类型的交集。 

第一种类型是在 T 上使用 Omit 实用程序类型以省略可分配给 KeyPart1 的字段的结果,在本例中为 a 字段。 第二种类型是您通过递归调用 NestedOmit 构建的新类型。

如果您进行 NestedOmit 的下一次评估,对于第一次递归调用,交集类型现在正在构建一个类型以用作 a 字段的类型。 这将重新创建一个没有您需要省略的嵌套字段的字段。

在 NestedOmit 的最终评估中,第一个条件将返回 false,因为传递的字符串类型现在只是“c”。 发生这种情况时,您可以使用内置助手从对象中省略该字段。 

这将返回 b 字段的类型,即省略了 c 的原始类型。 现在评估结束,TypeScript 返回您要使用的新类型,并省略嵌套字段。

结论

在本教程中,我们探索适用于函数、接口、类和自定义类型的泛型,以及使用了泛型来创建映射类型和条件类型。 

这些都使泛型成为您在使用 TypeScript 时可以随意使用的强大工具。 正确使用它们将使您免于一遍又一遍地重复代码,并使您编写的类型更加灵活。 

以上就是我今天跟你分享的全部内容,希望这些内容对你有所帮助。

Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞在看” 支持一波👍

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报