如何在 TypeScript 中创建自定义类型

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

介绍

TypeScriptJavaScript语言的扩展,使用JavaScript的运行时间与编译时间类型检查器。这种组合允许开发人员使用完整的JavaScript生态系统和语言功能,同时还添加可选的静态类型检查,列表,类别和界面。

虽然预制的(https://andsky.com/tech/tutorials/how-to-use-basic-types-in-typescript)将涵盖许多用例,但基于这些基本类型创建自己的自定义类型将允许您确保类型检查器验证特定于您的项目的数据结构。

本教程将向您展示如何使用TypeScript的自定义类型,如何组成这些类型,以及联盟和交叉,以及如何使用实用类型来增加您的自定义类型的灵活性。它将引导您通过不同的代码样本,您可以在自己的TypeScript环境中或在TypeScript播放场(https://www.typescriptlang.org/play?ts=4.2.2# ),一个在线环境,允许您直接在浏览器中写TypeScript。

前提条件

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

  • 联合国 您可以执行 TypeScript 程序的环境与示例一起执行 。 要在您的本地机器上设置此功能, 您需要以下操作 :
  • 安装了节点npm(或yarn),以便运行一个处理与脚本有关的TypeScript软件包的开发环境. 这个教程用Node.js版本14.3.0和npm版本6.14.5进行了测试. 要安装在 macOS 或 Ubuntu 18.04 上,请遵循 [如何在 macOS (https://andsky.com/tech/tutorials/how-to-install-node-js-and-create-a-local-development-environment-on-macos) 上安装节点并创建本地开发环境 或 使用 [如何在 Ubuntu 18.04 (https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-18-04 上安装节点.js 的PPA 部分。 如果您正在使用 Linux (WSL) (https://docs.microsoft.com/en-us/windows/wsl/install-win10) 的 Windows 子系统,此功能也会有效 。
  • 此外,您需要安装在您的机器上的TypeScript编译器。 要做到这一点,请参考官方TypeScript网站.
  • 联合国 如果您不想在本地机上创建 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.2创建的。

创建定制类型

在程序具有复杂数据结构的情况下,使用 TypeScript 的基本类型可能无法完全描述您正在使用的数据结构. 在这些情况下,声明自己的类型将帮助您解决复杂性。

定制类型语法

在TypeScript中,创建自定义类型的语法是使用类型的关键字,然后是类型名称,然后是与类型属性组成的{}块的分配。

1type Programmer = {
2  name: string;
3  knownFor: string[];
4};

语法类似于一个字面对象,其中密钥是属性的名称和值是该属性应该具有的类型。这定义了一个类型的)与具有字符串值的名称密钥和包含字符串数的knownFor密钥。

正如上面的示例所示,您可以使用 ; 作为每个属性之间的分离器,也可以使用一个字节, ,,或完全省略分离器,如下所示:

1type Programmer = {
2  name: string
3  knownFor: string[]
4};

使用自定义类型与使用任何基本类型相同. 添加双,然后添加类型名称:

1type Programmer = {
2  name: string;
3  knownFor: string[];
4};
5
6const ada: Programmer = {
7  name: 'Ada Lovelace',
8  knownFor: ['Mathematics', 'Computing', 'First Programmer']
9};

ada常数现在将通过类型检查器,而不会扔错误。

如果您在完全支持TypeScript的任何编辑器中编写此示例,例如在TypeScript Playground中,编辑器将建议该对象所期望的字段及其类型,如下面的动画所示:

An animation showing suggestions to add the "name" and "knownFor" key to a new instance of the "Programmer" type

如果您使用 TSDoc 格式添加评论,这是一种流行的 TypeScript 评论文档,它们也被建议在代码完成中。

 1type Programmer = {
 2  /**
 3   * The full name of the Programmer
 4   */
 5  name: string;
 6  /**
 7   * This Programmer is known for what?
 8   */
 9  knownFor: string[];
10};
11
12const ada: Programmer = {
13  name: 'Ada Lovelace',
14  knownFor: ['Mathematics', 'Computing', 'First Programmer']
15};

评论的描述现在将与场景建议一起出现:

Code completion with TSDoc comments

当使用自定义类型程序员创建对象时,如果您将具有意想不到类型的值分配给任何属性,TypeScript 将引发错误。

1type Programmer = {
2  name: string;
3  knownFor: string[];
4};
5
6const ada: Programmer = {
7  name: true,
8  knownFor: ['Mathematics', 'Computing', 'First Programmer']
9};

TypeScript 编译器(tsc)会显示错误 2322:

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

如果您错过了您类型所要求的任何属性,如下文:

1type Programmer = {
2  name: string;
3  knownFor: string[];
4};
5
6const ada: Programmer = {
7  name: 'Ada Lovelace'
8};

TypeScript 编译器会发出错误 2741:

1[secondary_label Output]
2Property 'knownFor' is missing in type '{ name: string; }' but required in type 'Programmer'. (2741)

添加未在原始类型中指定的新属性也会导致错误:

 1type Programmer = {
 2  name: string;
 3  knownFor: string[];
 4};
 5
 6const ada: Programmer = {
 7  name: "Ada Lovelace",
 8  knownFor: ['Mathematics', 'Computing', 'First Programmer'],
 9  age: 36
10};

在这种情况下,显示的错误是2322:

1[secondary_label Output]
2Type '{ name: string; knownFor: string[]; age: number; }' is not assignable to type 'Programmer'.
3Object literal may only specify known properties, and 'age' does not exist in type 'Programmer'.(2322)

定制类型

您也可以将自定义类型结合在一起。想象一下,您有一个公司类型,其中有一个经理字段,它遵循类型。

1type Person = {
2  name: string;
3};
4
5type Company = {
6  name: string;
7  manager: Person;
8};

然后,您可以创建类型的公司值,如下:

1const manager: Person = {
2  name: 'John Doe',
3}
4
5const company: Company = {
6  name: 'ACME',
7  manager,
8}

此代码将通过类型检查器,因为)来声明管理员

您可以忽略在经理常数中的类型,因为它与类型具有相同的形状。当您使用与经理属性类型预期的相同形状的对象时,TypeScript不会引发错误,即使它没有明确设置为具有类型。

以下不会引发错误:

1const manager = {
2  name: 'John Doe'
3}
4
5const company: Company = {
6  name: 'ACME',
7  manager
8}

您甚至可以更进一步,并将经理直接设置在这个公司对象中:

1const company: Company = {
2  name: 'ACME',
3  manager: {
4    name: 'John Doe'
5  }
6};

所有这些场景都是有效的。

如果您在支持 TypeScript 的编辑器中编写这些示例,您会发现编辑器将使用可用的类型信息来记录自己。

TypeScript Code Self-Documenting

现在您已经通过了创建您自己的自定义类型以固定数量的属性的一些示例,接下来您将尝试将可选属性添加到您的类型。

可选的属性

在以前的部分中使用自定义类型声明,您无法在创建该类型的值时忽略任何属性,但是有些情况需要可通过类型检查器的可选属性。

若要将可选属性添加到类型中,请将 ? 修改器添加到属性中. 使用前面部分中的 Programmer 类型,将 knownFor 属性转换为可选属性,通过添加以下突出字符:

1type Programmer = {
2  name: string;
3  knownFor?: string[];
4};

在这里,您在添加属性名称之后的 ? 修改器,这使得 TypeScript 将此属性视为可选,而在忽略该属性时不会引发错误:

1type Programmer = {
2  name: string;
3  knownFor?: string[];
4};
5
6const ada: Programmer = {
7  name: 'Ada Lovelace'
8};

这将没有错误的过去。

现在您已经知道如何向类型添加可选属性,现在是时候学习如何创建可以包含无限数量的字段的类型。

可索引的类型

在之前的示例中显示,如果该类型在声明时没有指定这些属性,则无法将属性添加到特定类型的值中,在本节中,您将创建可索引类型,这些类型是允许任何数量的字段,如果它们遵循类型的索引签名。

假设你有一个数据类型,可以拥有无限数量的任何类型的属性,你可以这样声明这个类型:

1type Data = {
2  [key: string]: any;
3};

在这里,您创建一个正常类型,在弯曲的轴承中使用类型定义块({}),然后在[key: typeOfKeys]: typeOfValues格式中添加一个特殊属性,其中typeOfKeys是该对象的键应该具有的类型,而typeOfValues是这些键的值应该具有的类型。

然后你可以像任何其他类型一样使用它:

1type Data = {
2  [key: string]: any;
3};
4
5const someData: Data = {
6  someBooleanKey: true,
7  someStringKey: 'text goes here'
8  // ...
9}

使用可索引类型,您可以分配无限数量的属性,只要它们匹配 index 签名,即用于描述可索引类型的密钥类型和值的名称。

您还可以添加可索引类型所需的特定属性,就像正常类型一样。

 1type Data = {
 2  status: boolean;
 3  [key: string]: any;
 4};
 5
 6const someData: Data = {
 7  status: true,
 8  someBooleanKey: true,
 9  someStringKey: 'text goes here'
10  // ...
11}

这意味着一个数据类型对象必须具有具有布尔值的状态密钥才能通过类型检查器。

现在,您可以创建具有不同的元素数量的对象,您可以继续学习TypeScript中的数组,其中可以有自定义元素数量或更多。

创建具有元素数量或更多元素的数组

使用在TypeScript中可用的两个基本类型(https://andsky.com/tech/tutorials/how-to-use-basic-types-in-typescript# basic-types-used-in-typescript),您可以创建具有最小元素数量的数组的自定义类型。 在本节中,您将使用TypeScript [rest 操作员](https://andsky.com/tech/tutorials/understanding-destructuring-rest-parameters-and-spread-syntax-in-javascript rest-parameters) ...来做到这一点。

假设你有一个负责合并多个字符串的函数. 这个函数将采取一个单一的数组参数。 这个数组必须有至少两个元素,每个元素都应该是字符串。

1type MergeStringsArray = [string, string, ...string[]];

MergeStringsArray类型正在利用你可以使用与数组类型的休息操作员的事实,并将其结果用作一个tuple的第三个元素,这意味着需要前两个字符串,但之后不需要额外的字符串元素。

如果一个数组有少于两个字符串元素,它将无效,如下列情况:

1const invalidArray: MergeStringsArray = ['some-string']

TypeScript 编译器在检查此数组时会发出错误 2322:

1[secondary_label Output]
2Type '[string]' is not assignable to type 'MergeStringsArray'.
3Source has 1 element(s) but target requires 2. (2322)

到目前为止,您已经从基本类型的组合中创建了自己的自定义类型,在下一节中,您将通过组合两个或多个自定义类型来创建一个新的类型。

组成类型

本节将通过两种方式来组成类型,这些方法将使用 _union 运算符传输附加到一种或另一种类型的任何数据,以及 _intersection 运算符传输满足两种类型的所有条件的数据。

工会

使用)运算符创建联盟,该运算符代表可以具有联盟中的任何类型的值。

1type ProductCode = number | string

在此代码中,‘ProductCode’可以是‘字符串’或‘数字’,以下代码将通过类型检查器:

1type ProductCode = number | string;
2
3const productCodeA: ProductCode = 'this-works';
4
5const productCodeB: ProductCode = 1024;

可以从任何有效的 TypeScript 类型的联盟中创建联盟类型。

交叉

您可以使用交叉类型创建一个具有所有交叉类型的所有属性的全新类型。

例如,假设您有某些常见的字段,这些字段总是出现在 API 调用响应中,然后是某些终端的特定字段:

 1type StatusResponse = {
 2  status: number;
 3  isValid: boolean;
 4};
 5
 6type User = {
 7  name: string;
 8};
 9
10type GetUserResponse = {
11  user: User;
12};

在这种情况下,所有响应都将具有状态isValid属性,但只有用户响应将有额外的用户字段。

1type ApiGetUserResponse = StatusResponse & GetUserResponse;

ApiGetUserResponse类型将具有在StatusResponseGetUserResponse中可用的所有属性,这意味着数据只会在满足两种类型的所有条件的情况下通过类型检查器。

1let response: ApiGetUserResponse = {
2    status: 200,
3    isValid: true,
4    user: {
5        name: 'Sammy'
6    }
7}

另一个例子是数据库客户端返回的行类型,用于包含 joins 的查询,您可以使用交叉类型来指定此类查询的结果:

1type UserRoleRow = {
2  role: string;
3}
4
5type UserRow = {
6  name: string;
7};
8
9type UserWithRoleRow = UserRow & UserRoleRow;

之后,如果您使用了fetchRowsFromDatabase()函数,如下:

1const joinedRows: UserWithRoleRow = fetchRowsFromDatabase()

由此产生的常数joinedRows必须具有一个角色属性和一个名称属性,这两个属性都持有字符串值,以便通过类型检查器。

使用 Template Strings 类型

从 TypeScript 4.1 开始,您可以使用 template string类型创建类型,这将允许您创建类型,以检查特定字符串格式,并为您的 TypeScript 项目添加更多定制。

要创建模板字符串类型,您使用的语法几乎与创建模板字符串时使用的字符串类型相同。

想象一下,您想要创建一个通过get开始的所有字符串的类型,您可以使用模板字符串类型来完成此操作:

1type StringThatStartsWithGet = `get${string}`;
2
3const myString: StringThatStartsWithGet = 'getAbc';

myString将通过此类型检查器,因为字符串从get开始,然后是额外的字符串。

如果您将无效值传递给类型,如下列无效StringValue:

1type StringThatStartsWithGet = `get${string}`;
2
3const invalidStringValue: StringThatStartsWithGet = 'something';

TypeScript 编译器会给您错误 2322:

1[secondary_label Output]
2Type '"something"' is not assignable to type '`get${string}`'. (2322)

使用模板字符串创建类型可帮助您将类型定制为项目的特定需求,在下一节中,您将尝试类型声明,这些类型将添加到其他未输入的数据中。

使用类型声明

[ " 任何 " 类型] (https://andsky.com/tech/tutorials/how-to-use-basic-types-in-typescript# basic-types-used-in-typescript)可用作任何值的类型,往往不提供从TypeScript中获取全部好处所需的强打字。 但有时,你可能会遇到一些与你无法控制的`任何'有联系的变量。 如果您正在使用未写入 TypeScript 或没有 [可用类型声明] (https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html ) 的外部依赖关系,则会发生这种情况.

如果你想在这些场景中让你的代码类型安全,你可以使用类型声明,这是改变变量的类型到另一种类型的一种方式。

举以下例子:

1const valueA: any = 'something';
2
3const valueB = valueA as string;

valueA有类型任何,但使用as关键字,此代码迫使valueB具有类型string

<$>[注] 注: 若要声称一个TypeA变量具有TypeB类型,则TypeB必须是TypeA子类型。

用途类型

在之前的部分中,您审查了从基本类型中创建自定义类型的多种方法,但有时您不希望从头开始创建一个全新的类型。

所有这些都可以通过使用现有的工具类型来实现,这部分将涵盖一些这些工具类型;对于所有可用的工具类型的完整列表,请参阅(https://www.typescriptlang.org/docs/handbook/utility-types.html)的TypeScript手册部分。

所有实用类型都是 Generic Types,你可以将其视为接受其他类型作为参数的类型。

「記錄<鍵,價值>」

可以使用记录工具类型以更清洁的方式创建可索引的类型,而不是使用以前涵盖的索引签名。

在您的可索引类型示例中,您有以下类型:

1type Data = {
2  [key: string]: any;
3};

您可以使用记录工具类型,而不是这样的可索引类型:

1type Data = Record<string, any>;

「記錄」一般的第一個類型參數是每個「鍵」的類型. 在下面的例子中,所有鍵必須是字符串:

1type Data = Record<string, any>

第二种类型的参数是这些密钥的每个的类型,如下所述,则允许这些值为任何值:

1type Data = Record<string, any>

Omit<Type, 字段>

Omit实用工具类型有助于创建基于另一个新的类型,同时在结果类型中排除一些您不想要的属性。

假设您有以下类型来表示数据库中的用户行类型:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};

如果在代码中你正在检索所有字段,但addressId一个,你可以使用Omit创建一个新的类型,而没有该字段:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};
7
8type UserRowWithoutAddressId = Omit<UserRow, 'addressId'>;

Omit的第一个参数是您基于新类型的类型,第二个是您想要省略的字段。

如果您在代码编辑器中浏览UserRowWithoutAddressId,您会发现它具有UserRow类型的所有属性,但您错过的属性。

您可以使用字符串联盟将多个字段传输到第二种类型的参数. 假设您还想省略id字段,您可以这样做:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};
7
8type UserRowWithoutIds = Omit<UserRow, 'id' | 'addressId'>;

Pick<Type, 字段>

选择实用程序类型与Omit类型完全相反,而不是说要省略的字段,您指定您想从另一个字段中使用的字段。

使用您之前使用的相同的UserRow:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};

假设您只需要从数据库行选择电子邮件密钥,您可以使用选择来创建此类型:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};
7
8type UserRowWithEmailOnly = Pick<UserRow, 'email'>;

在这里,第一个对选择的参数指定您正在基于新类型的类型;第二个是您想要包含的密钥。

这将相当于如下:

1type UserRowWithEmailOnly = {
2    email: string;
3}

您还可以使用串联选择多个字段:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};
7
8type UserRowWithEmailOnly = Pick<UserRow, 'name' | 'email'>;

一部分``一部分

使用相同的UserRow示例,想象您想要创建一个与数据库客户端可用于插入新数据的对象的新类型,但有一个小细节:您的数据库对所有字段都有默认值,因此您不需要通过任何字段。

您的现有类型UserRow具有所需的所有属性:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};

若要创建一个所有属性可选的新类型,可以使用部分<类型>工具类型,如下:

1type UserRow = {
2  id: number;
3  name: string;
4  email: string;
5  addressId: string;
6};
7
8type UserRowInsert = Partial<UserRow>;

这是完全相同的,你的UserRowInsert是这样的:

 1type UserRow = {
 2  id: number;
 3  name: string;
 4  email: string;
 5  addressId: string;
 6};
 7
 8type UserRowInsert = {
 9  id?: number | undefined;
10  name?: string | undefined;
11  email?: string | undefined;
12  addressId?: string | undefined;
13};

实用工具类型是一个很好的资源,因为它们提供了比在TypeScript中从基本类型创建类型更快的方式。

结论

创建自己的自定义类型来代表您自己的代码中使用的数据结构可以为您的项目提供灵活和有用的TypeScript解决方案. 除了增加您自己的代码整体的类型安全性外,在代码中输入自己的业务对象作为数据结构将增加代码库的整体文档,并在与同事一起在同一代码库上工作时提高您自己的开发人员体验。

有关TypeScript的更多教程,请参阅我们的(https://www.digitalocean.com/community/tutorial_series/how-to-code-in-typescript)。

Published At
Categories with 技术
comments powered by Disqus