如何在 TypeScript 中使用泛型

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

Generics 是静态编码语言的一个基本特征,允许开发人员将 types作为参数传递给另一种类型, function或其他结构. 当开发人员将其组件变成一种通用组件时,他们会让该组件能够接受并执行在使用组件时传递的编码,从而提高代码的灵活性,使组件可重复使用,并消除重复。

TypeScript完全支持通用语法,以便在接受参数的组件中引入类型安全,并返回值,其类型将不确定,直到它们在您的代码中被消耗。 在本教程中,您将尝试真实世界的TypeScript通用语法示例,并探索如何在函数,类型, 接口中使用它们。

前提条件

要遵循本教程,您将需要:

  • 联合国 您可以执行 TypeScript 程序的环境与示例一起执行 。 要在您的本地机上设置此功能, 您需要安装如下:
  • 既 [Node] (https://nodejs.org/about/) (又 [npm] (https://docs.npmjs.com/about-npm (或 [yarn] (https://yarnpkg.com/getting-started )) , 以便运行一个处理 TypeScript相关包的开发环境. 要安装在 macOS 或 Ubuntu 20.04 上, 请遵循 [如何在 macOS (https://andsky.com/tech/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos ) 上安装节点并创建本地开发环境的步骤 或 使用节点源 PPA** 部分的** Installing Node.js with Apt Ubuntu 20.04 (https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-18-04) 。 如果您正在使用 Linux (WSL) (https://docs.microsoft.com/en-us/windows/wsl/install-win10) 的 Windows 子系统,此功能也会有效 。
  • 此外,您需要安装在您的机器上的脚本编译器。 要做到这一点,请参考官方TypeScript网站.
  • 如果您不想在本地机器上创建类型脚本环境, 您可以使用官方 [TypeScript Playground] (https://www.typescriptlang.org/play) 来跟踪 。
  • 您需要足够的JavaScript知识,特别是ES6+语法,例如解构,休息操作员,以及进出口. 如果您需要更多有关这些主题的信息,请读取我们的[JavaScript series How To Code (https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript).
  • 此教程会引用文本编辑器中支持TypeScript并显示行内错误的方面. 这对使用TypeScript是不必要的,但确实更多地利用了TypeScript的特性. 为了获得这些好处,您可以使用像[Visual Studio Code (https://code.visualstudio.com/)这样的文本编辑器,该编辑器对出框的TypeScript有完全的支持. 您可以在 [TypeScript Playground] (https://www.typescriptlang.org/play ) 中尝试这些好处. (英语)

本教程中显示的所有示例都是使用TypeScript版本 4.2.3创建的。

通用语法

在进入通用语法的应用之前,本教程首先将通过TypeScript通用语法的语法,然后是示例来说明它们的通用目的。

通用元素出现在 TypeScript 代码中,在<T>格式中,其中T代表一个传入类型。<T>可以被读取为T类型的通用元素。在这种情况下,T将以相同的方式运作,参数在函数中工作,作为在创建结构实例时将声明的类型的位置持有人。

<$>[注] **注: ** 根据惯例,程序员通常会使用单个字母来命名通用类型. 这不是一个语法规则,你可以像TypeScript中的任何其他类型一样命名通用类型,但这种惯例有助于立即向那些阅读你的代码的人传达通用类型不需要特定类型。

通用元素可以出现在函数、类型、类别和界面中,这些结构将在本教程中稍后描述,但目前,一个函数将被用作示例来说明通用元素的基本语法。

要查看通用函数有多么有用,请想象你有一个 JavaScript 函数,它需要两个参数:一个 object和一个 array的键。

1function pickObjectKeys(obj, keys) {
2  let result = {}
3  for (const key of keys) {
4    if (key in obj) {
5      result[key] = obj[key]
6    }
7  }
8  return result
9}

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

以下是一个示例,说明如何使用该函数:

1const language = {
2  name: "TypeScript",
3  age: 8,
4  extensions: ['ts', 'tsx']
5}
6
7const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])

这声明一个对象为语言,然后将年龄扩展属性与pickObjectKeys()函数隔离。

1{
2  age: 8,
3  extensions: ['ts', 'tsx']
4}

如果你要将此代码迁移到TypeScript,以使其安全打字,你将不得不使用通用代码. 您可以通过添加以下突出的行来重构代码:

 1function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {
 2  let result = {} as Pick<T, K>
 3  for (const key of keys) {
 4    if (key in obj) {
 5      result[key] = obj[key]
 6    }
 7  }
 8  return result
 9}
10
11const language = {
12  name: "TypeScript",
13  age: 8,
14  extensions: ['ts', 'tsx']
15}
16
17const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])

<T,K扩展 keyof T> 声明函数的两个参数类型,其中 K 被分配给一个类型,它是 T 中的密钥的联合。 obj 函数参数随后被设置为任何代表 T 的类型,并且 keys' 被分配给任何代表 K` 的类型。

这会强制执行基于pickObjectKeys提供的参数的返回类型,使函数在知道需要强制执行的特定类型之前能够灵活地强制执行键结构。

TypeScript suggestion based on the type of the object

有了如何在TypeScript中创建通用元素的想法,您现在可以继续探索在特定情况下使用通用元素,本教程将首先涵盖如何在函数中使用通用元素。

使用具有功能的通用品

使用具有函数的通用程序最常见的场景之一是,当您有某些代码不易在所有用例中输入时。 为了使该函数应用于更多情况,您可以包括通用键入。 在此步骤中,您将通过一个身份函数示例来说明这一点。

指定一般参数

查看以下函数,该函数返回了作为第一个参数传递的内容:

1function identity(value) {
2  return value;
3}

您可以添加以下代码来使函数在TypeScript中安全打字:

1function identity<T>(value: T): T {
2  return value;
3}

您将函数转换为接受第一个参数类型的通用类型参数 T 的通用函数,然后将返回类型设置为 :T

接下来,添加以下代码来尝试该函数:

1function identity<T>(value: T): T {
2  return value;
3}
4
5const result = identity(123);

「結果」的類型是「123」,這是您所傳遞的準確數字。TypeScript 在這裡是從呼叫代碼本身推斷通用類型。這樣,呼叫代碼不需要傳遞任何類型參數。

1function identity<T>(value: T): T {
2  return value;
3}
4
5const result = identity<number>(123);

在此代码中,结果的类型为数字。通过输入<数字>代码的类型,您正在明确告知TypeScript,您希望身份函数的通用类型参数T的类型为数字

直接通过类型参数

使用自定义类型时,直接传递类型参数也很有用,例如,请查看以下代码:

1type ProgrammingLanguage = {
2  name: string;
3};
4
5function identity<T>(value: T): T {
6  return value;
7}
8
9const result = identity<ProgrammingLanguage>({ name: "TypeScript" });

在此代码中,result具有自定义类型ProgrammingLanguage,因为它被直接传输到identity函数中。

在使用JavaScript时,另一个常见的例子是使用包装函数从API中获取数据:

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

非同步函数将 URL 路径作为参数,使用 fetch API向 URL 提出请求,然后返回 JSON响应值。

任何作为返回类型并不太有用。任何意味着任何JavaScript值,并且通过使用它,您正在失去静态类型检查,这是TypeScript的主要好处之一。

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

突出的代码将您的函数转化为接受ResultType一般类型参数的通用函数,该通用类型用于函数的返回类型:Promise<ResultType>

<$>[注] 注: 由于您的函数是async,您必须返回一个Promise对象。

如果您仔细看看您的函数,您会看到,在参数列表中或其他任何地方,TypeScript 都无法推断其值,这意味着呼叫代码在呼叫函数时必须明确通过该通用代码的类型。

以下是fetchApi通用函数的可能实现,以获取用户数据:

 1type User = {
 2  name: string;
 3}
 4
 5async function fetchApi<ResultType>(path: string): Promise<ResultType> {
 6  const response = await fetch(`https://example.com/api${path}`);
 7  return response.json();
 8}
 9
10const data = await fetchApi<User[]>('/users')
11
12export {}

在此代码中,您正在创建一个名为用户的新类型,并使用该类型的数组(用户 )作为ResultType通用参数的类型。

<$>[注] 注: 当您使用等待来非同步处理函数的结果时,返回类型将是Promise<T>中的T类型,在这种情况下是通用类型ResultType

默认参数类型

创建你的通用fetchApi函数就像你正在做的那样,调用代码总是必须提供类型参数. 如果调用代码不包括通用类型,则ResultType将被绑定到未知

 1async function fetchApi<ResultType>(path: string): Promise<ResultType> {
 2  const response = await fetch(`https://example.com/api${path}`);
 3  return 
 4response.json();
 5}
 6
 7const data = await fetchApi('/users')
 8
 9console.log(data.a)
10
11export {}

此代码试图访问数据的理论a属性,但由于数据类型是未知,因此此代码将无法访问对象的属性。

如果您不打算在您的通用函数的每个调用中添加特定类型,则可以将默认类型添加到通用类型参数中。

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

使用此代码,在调用fetchApi函数时,您不再需要将类型传输到ResultType通用参数,因为它具有默认类型Record<string, any>

参数类型限制

在某些情况下,一个通用类型参数只需要允许某些形状被传入通用类型,以便为通用类型创建这种额外的特异性层,您可以对参数设置限制。

假设您有一个存储限制,您只允许存储具有所有属性的字符串值的对象。 为此,您可以创建一个函数,该函数取代任何对象,并返回另一个对象与原来的钥匙相同,但其所有值都转换为字符串。

此函数将是一个通用函数. 这样,您可以使结果的对象与原始对象具有相同的形状。

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

在此代码中, stringifyObjectKeyValues 使用 `reduce' 数组方法在原始密钥的数组上迭代,串行值并将它们添加到新的数组中。

要确保调用代码总是会将一个对象传递给您的函数,您正在使用通用类型限制T,如下所示的突出代码所示:

1function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
2  // ...
3}

extends Record<string, any> 被称为通用类型限制,它允许您指定您的通用类型必须可以分配给‘extends’ 关键字之后的类型。

在调用减少时,减量函数的返回类型是基于电池的初始值的。 代码{} as {K in keyof T]: string }将电池的初始值的类型设置为{ [K in keyof T]: string }通过在空对象上使用一种类型 cast,{}。 类型{ [K in keyof T]: string }创建了一个新类型,具有与T相同的键,但所有值都设置为string类型。

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

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

变量 stringifiedValues 将具有以下类型:

1{
2  a: string;
3  b: string;
4  c: string;
5  d: string;
6}

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

本节涵盖了使用功能的通用药物的多种方法,包括直接分配类型参数并对参数形状进行默认和限制。

使用与接口,类和类型的通用产品

在 TypeScript 中创建 接口时,使用通用类型参数来设置结果对象的形状可能是有用的。

通用接口和类

要创建一个通用界面,您可以添加类型参数列表,即在界面名称后:

1interface MyInterface<T> {
2  field: T
3}

这声明具有属性字段的界面,其类型由传入到T的类型决定。

对于类,它几乎是相同的语法:

1class MyClass<T> {
2  field: T
3  constructor(field: T) {
4    this.field = field
5  }
6}

通用接口/类的一个常见用例是,当您有一个字段,其类型取决于客户端代码如何使用接口/类时,假设您有一个HttpApplication类,用于处理HTTP请求到您的API,并且某些背景值将传递给每个请求处理器。

 1class HttpApplication<Context> {
 2  context: Context
 3    constructor(context: Context) {
 4    this.context = context;
 5  }
 6
 7  // ... implementation
 8
 9  get(url: string, handler: (context: Context) => Promise<void>): this {
10    // ... implementation
11    return this;
12  }
13}

此类存储一个背景,其类型被传入为收到方法中的处理函数的参数类型。

1...
2const context = { someValue: true };
3const app = new HttpApplication(context);
4
5app.get('/api', async () => {
6  console.log(context.someValue)
7});

在此实现中,TypeScript 会将context.someValue 类型推断为boolean

基因类型

现在您已经通过了类和接口中的一些通用元素的示例,现在您可以转向创建通用自定义类型。 将通用元素应用于类型的语法类似于它们如何应用于接口和类型。

1type MyIdentityType<T> = T

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

1...
2type B = MyIdentityType<number>

在这种情况下,类型的B将是类型的数字

通用类型通常用于创建辅助类型,特别是在使用地图类型时。TypeScript提供了许多预先构建的辅助类型。其中一个例子是部分类型,该类型采用一个类型T,并返回另一个类型,其形状与T相同,但其所有字段都设置为可选。

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

在这里,类型部分采用一个类型,重复其属性类型,然后在新类型中返回作为可选类型。

<$>[注] **注:**由于部分已经嵌入到TypeScript中,将此代码编译到您的TypeScript环境中会重新声明部分并引发错误。

要看到一般类型有多强大,想象一下你有一个字面上的对象,它存储从一个商店到您的业务分销网络中的所有其他商店的运输成本。

 1{
 2  ABC: {
 3    ABC: null,
 4    DEF: 12,
 5    GHI: 13,
 6  },
 7  DEF: {
 8    ABC: 12,
 9    DEF: null,
10    GHI: 17,
11  },
12  GHI: {
13    ABC: 13,
14    DEF: 17,
15    GHI: null,
16  },
17}

此物件是代表商店位置的物件集合。在每个商店位置中,有代表其他商店的运输成本的物品。例如,从ABCDEF的运输成本为12

为了确保其他商店的位置具有一致的价值,并且商店发货到自己总是,您可以创建一个通用辅助类型:

1type IfSameKeyThanParentTOtherwiseOtherType<Keys extends string, T, OtherType> = {
2  [K in Keys]: {
3    [SameThanK in K]: T;
4  } &
5    { [OtherThanK in Exclude<Keys, K>]: OtherType };
6};

类型IfSameKeyThanParentTOotherType收到三种通用类型。第一个类型Keys是你想要确保你的对象拥有的所有密钥。在这种情况下,它是所有商店的代码的联盟。T是当嵌入的对象字段与主对象的密钥相同时的类型,这在这种情况下代表一个商店发送到自己的位置。

你可以这样使用它:

 1...
 2type Code = 'ABC' | 'DEF' | 'GHI'
 3
 4const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
 5  ABC: {
 6    ABC: null,
 7    DEF: 12,
 8    GHI: 13,
 9  },
10  DEF: {
11    ABC: 12,
12    DEF: null,
13    GHI: 17,
14  },
15  GHI: {
16    ABC: 13,
17    DEF: 17,
18    GHI: null,
19  },
20}

如果您将任何键设置为无效值,TypeScript会给我们一个错误:

 1...
 2const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
 3  ABC: {
 4    ABC: 12,
 5    DEF: 12,
 6    GHI: 13,
 7  },
 8  DEF: {
 9    ABC: 12,
10    DEF: null,
11    GHI: 17,
12  },
13  GHI: {
14    ABC: 13,
15    DEF: 17,
16    GHI: null,
17  },
18}

由于ABC和自身之间的发送成本不再是null,TypeScript会引发以下错误:

1[secondary_label Output]
2Type 'number' is not assignable to type 'null'.(2322)

您现在已经尝试了在界面,类和自定义助手类型中使用通用药物,接下来,您将进一步探索本教程中已经出现过几次的主题:使用通用药物创建地图类型。

使用通用药物创建地图类型

在使用 TypeScript 工作时,有时需要创建一个类型,该类型应该与其他类型具有相同的形状,这意味着它应该具有相同的属性,但属性类型设置为不同的。

在TypeScript中,这种结构被称为 mapped type,并依赖于通用元素。

假设您想要创建一个类型,该类型会返回一个新的类型,所有属性都设置为具有boolean值,您可以使用以下代码创建此类型:

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

在此类型中,您正在使用语法 [K in keyof T]' 来指定新类型将具有的属性。 keyof T运算器被用来返回一个与T` 中可用的所有属性的名称的联盟。

这会创建一个名为K的新类型,该类型与当前属性的名称有关。

此类型的BooleanFields的一个使用场景是创建一个选项对象. 假设您有一个数据库模型,例如用户。 当您从数据库中获取该模型的记录时,您还将允许传递一个指定返回哪些字段的对象。

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

 1type BooleanFields<T> = {
 2  [K in keyof T]: boolean;
 3};
 4
 5type User = {
 6  email: string;
 7  name: string;
 8}
 9
10type UserFetchOptions = BooleanFields<User>;

在本示例中,UserFetchOptions将与创建它相同:

1type UserFetchOptions = {
2  email: boolean;
3  name: boolean;
4}

创建地图类型时,您还可以为这些字段提供修改器。其中一个例子是TypeScript中现有的通用类型,名为Readonly<T>Readonly<T>类型返回一个新的类型,通过类型的所有属性都设置为readonly属性。

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

<$>[注] **注:**由于Readonly已经嵌入到TypeScript中,将此代码编译到您的TypeScript环境中会重新声明Readonly并引发错误。

请注意在本代码中添加到K in keyof T部分作为前缀的readonly编辑器。目前,可用于地图类型的两个可用编辑器是readonly编辑器,该编辑器必须作为前缀添加到属性,以及?编辑器,该编辑器可以作为后缀添加到属性中。

现在,您可以使用绘制类型来创建基于您已经创建的类型形状的新类型,您可以转到通用药物的最终用例:条件键。

使用通用药物创建条件类型

在本节中,您将尝试 TypeScript 中的另一个有用的通用功能:创建条件类型. 首先,您将通过条件键的基本结构进行运行. 然后,您将通过创建一个基于点符号的对象类型的嵌入字段的条件类型来探索一个先进的用例。

条件打字的基本结构

条件类型是具有不同结果类型的通用类型,取决于某些条件(https://andsky.com/tech/tutorials/how-to-write-conditional-statements-in-javascript)。

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

在此代码中,您正在创建一个名为IsStringType的新通用类型,该类型收到一个单一的类型参数T。在您的类型定义中,您正在使用一种使用JavaScript中的三重运算器的条件表达式的语法:T扩展字符串? true : false

<$>[注] 注: 此条件式表达式在编译过程中被评估。TypeScript仅适用于类型,因此请确保在类型声明中始终将标识符读取为类型,而不是作为值。

要尝试这个条件类型,通过一些类型作为其类型参数:

1type IsStringType<T> = T extends string ? true : false;
2
3type A = "abc";
4type B = {
5  name: string;
6};
7
8type ResultA = IsStringType<A>;
9type ResultB = IsStringType<B>;

在此代码中,您正在创建两种类型,AB。类型A是字母字符串abc的类型,而类型B是具有类型string名称属性的对象的类型。

如果你检查结果类型ResultAResultB,你会注意到ResultA类型被设置为确切类型,而ResultB类型被设置为

条件类型的一个有用的特征是,它允许您通过特殊的关键字infer来推断扩展条款中的类型信息,然后可以在条件的真实分支中使用这种新类型。

输入以下GetReturnType类型来说明这一点:

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

在此代码中,您正在创建一个新的通用类型,即被称为GetReturnType的条件类型。这个通用类型接受一个单一的类型参数,即T。在类型声明本身中,您正在检查T类型是否扩展一个与函数签名相匹配的类型,该类型接受参数的变数(包括零),然后您正在推断该函数的返回类型,创建一个新的类型U,可用于条件的真实分支。

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

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

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

由于您的someFunction变量类型是函数,所以条件类型会评估条件的真实分支。这将返回结果的U类型。从函数的返回类型中得出U类型,在这种情况下是boolean。如果你检查ReturnTypeOfSomeFunction类型,你会发现它正确设置为boolean类型。

先进条件类型使用案例

在本节中,您将通过创建一个名为NestedOmit<T, KeysToOmit>的条件类型来探索其中一个用例,该用途类型将能够在对象中排除字段,就像现有的Omit<T, KeysToOmit>用途类型一样(https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys),但也将允许通过使用点标注排除嵌入的字段。

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

 1type SomeType = {
 2  a: {
 3    b: string,
 4    c: {
 5      d: number;
 6      e: string[]
 7    },
 8    f: number
 9  }
10  g: number | string,
11  h: {
12    i: string,
13    j: number,
14  },
15  k: {
16    l: number,<F3>
17  }
18}
19
20type Result = NestedOmit<SomeType, "a.b" | "a.c.e" | "h.i" | "k">;

此代码声明一个名为SomeType的类型,它具有多层结构的嵌入属性。 使用您的NestedOmit通用词,您通过类型,然后列出您想要省略的属性的密钥。 注意如何在第二个类型参数中使用点符号来识别要省略的密钥。

构建此条件类型将使用TypeScript中可用的许多功能,例如模板字面类型、通用类型、条件类型和地图类型。

要尝试这种通用产品,首先要创建一个名为NestedOmit的通用类型,该类型接受两个类型参数:

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

第一种类型参数称为T,必须是可以分配到Record<string, any>类型的类型,这将是您想要排除属性的对象类型,第二种类型参数称为KeysToOmit,必须是string类型。

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

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

在这里,您正在使用模板的字母字符串类型,同时利用条件类型来推断模板的字符串类型内部的另外两个类型。 通过推断模板的字符串类型的两个部分,您正在将字符串分成两个其他字符串。 第一部分将被分配给类型 KeyPart1,并将包含在第一个点之前的一切。 第二部分将被分配给类型 KeyPart2,并将包含在第一个点之后的一切。

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

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

此方法使用 KeyPart1 扩展 keyof T 来检查 KeyPart1' 是否是给定类型的有效属性 T`. 如果您有有效的密钥,请添加以下代码,以使条件评估到两种类型的交叉点:

1type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
2  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
3    ?
4      KeyPart1 extends keyof T
5      ?
6        Omit<T, KeyPart1>
7        & {
8          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
9        }

「Omit<T, KeyPart1>」是使用与TypeScript默认交付的「Omit」辅助器构建的类型,此时「KeyPart1」没有点符号:它将包含包含您想从原始类型中排除的嵌入字段的字段的确切名称。

您正在使用Omit来删除在T[KeyPart1]内部的一些嵌套字段,而要做到这一点,您必须重建T[KeyPart1]类型。 为了避免重建整个T类型,您使用Omit来从T中删除KeyPart1,保留其他字段。

「[NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>'是一个地图类型,属性是可以分配给 KeyPart1的,这意味着你刚刚从 NestedToOmit中提取的部分。这是你想要删除的字段的母。如果你通过 a.b.c,在你的条件的第一次评估时,它将是 NewKeys in '。然后,你将该属性的类型设置为重新调用你的 NestedOmit实用类型的结果,但现在通过作为第一个类型参数,使用 T内的该属性类型使用 T[NewKeys],并作为第二类型参数传递在 KeyPart2'中可用的非分配中剩余的密

在内部条件的分支中,返回与T相关的当前类型,就好像KeyPart1不是一个有效的T密钥:

 1type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
 2  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
 3    ?
 4      KeyPart1 extends keyof T
 5      ?
 6        Omit<T, KeyPart1>
 7        & {
 8          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
 9        }
10      : T

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

最后,在外部条件的分支中,使用现有的Omit实用工具类型将KeysToOmitType中省略:

 1type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
 2  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
 3    ?
 4      KeyPart1 extends keyof T
 5      ?
 6        Omit<T, KeyPart1>
 7        & {
 8          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
 9        }
10      : T
11    : Omit<T, KeysToOmit>;

如果条件KeysToOmit 扩展到 ${infer KeyPart1}.${infer KeyPart2}false,则意味着KeysToOmit不使用点符号,因此您可以使用现有的Omit实用类型。

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

 1type NestedObject = {
 2  a: {
 3    b: {
 4      c: number;
 5      d: number;
 6    };
 7    e: number;
 8  };
 9  f: number;
10};

然后拨打NestedOmit,以省略在a.b.c中可用的嵌入字段:

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

在第一个条件类型的评估中,外部条件将是,因为字符串类型a.b.c可以归因于模板字符串类型`${infer KeyPart1}.${infer KeyPart2}``。

这将评估为真实,因为KeyPart1在这个时刻是T的关键。

 1type NestedObject = {
 2  a: {
 3    b: {
 4      c: number;
 5      d: number;
 6    };
 7    e: number;
 8  };
 9  f: number;
10};

随着条件的评估,你现在在内部的分支中。这构建了一个新的类型,它是两种其他类型的交叉点。第一个类型是使用Omit实用类型在T上排除可归因于KeyPart1的字段的结果,在这种情况下是a字段。

如果您通过NestedOmit的下一个评估,对于第一个重复呼叫,交叉类型现在正在构建一种用于a字段的类型。

NestedOmit的最终评估中,第一个条件会返回false,因为通过的字符串类型现在只是c。当这种情况发生时,您会使用内置的帮助器将对象的字段放弃。

结论

在本教程中,您将探索通用语法,因为它们适用于函数,界面,类和自定义类型。您还使用通用语法创建地图和条件类型。 这些都使通用语法成为您使用TypeScript时可用的强大工具。 正确使用它们将节省您反复重复代码,并使您所写的类型更加灵活。 这尤其适用于您是图书馆的作者,并计划让您的代码可读于广泛的受众。

有关TypeScript的更多教程,请参阅我们的 How To Code in TypeScript 系列页面

Published At
Categories with 技术
comments powered by Disqus