如何在 TypeScript 中使用接口

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

介绍

TypeScript是使用JavaScript运行时间与编译时间类型检查器的JavaScript(https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript)语言的扩展。

TypeScript 提供多种方式来表示代码中的对象,其中一种是使用界面。在 TypeScript 中,界面有两种使用场景:您可以创建一个必须遵循的类合同,例如这些类必须执行的成员,您也可以在应用程序中表示类型,就像正常的类型声明一样(有关类型的更多信息,请参阅 如何在 TypeScript 中使用基本类型如何在 TypeScript 中创建定制类型

您可能会注意到界面和类型共享相似的特征集;事实上,一个几乎总是可以取代另一个。 主要的区别是界面可能具有相同界面的多个声明,而TypeScript会合并,而类型只能声明一次。

TypeScript 中的接口是代表类型结构的强大方法,它们允许您使用这些结构进行类型安全,并同时记录它们,直接提高开发人员的体验。

在本教程中,您将创建TypeScript界面,学习如何使用它们,并了解正常类型和界面之间的差异.您将尝试不同的代码样本,您可以在自己的TypeScript环境或TypeScript Playground(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 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中可用的不同功能创建界面,您还将学习如何使用您创建的界面。

TypeScript 中的接口是使用接口的关键字创建的,其次是接口的名称,然后是接口的体组的{}块。

1interface Logger {
2  log: (message: string) => void;
3}

类似于使用类型声明创建正常类型,您可以在{}中指定类型的字段及其类型:

1interface Logger {
2  log: (message: string) => void;
3}

Logger接口代表一个具有一个名为log的属性的对象,该属性是接受一个单个类型的 string参数并返回的函数。

您可以像任何其他类型一样使用Logger接口. 以下是创建一个与Logger接口相匹配的对象字母的示例:

1interface Logger {
2  log: (message: string) => void;
3}
4
5const logger: Logger = {
6  log: (message) => console.log(message),
7};

使用Logger界面的值必须具有与Logger界面声明中指定的成员相同的类型。

由于值必须遵循界面中声明的内容,所以添加外部字段会导致编译错误,例如,在文本对象中,请尝试添加在界面中缺少的新属性:

1interface Logger {
2  log: (message: string) => void;
3}
4
5const logger: Logger = {
6  log: (message) => console.log(message),
7  otherProp: true,
8};

在这种情况下,TypeScript 编译器会发出2322错误,因为这个属性在Logger界面声明中不存在:

1[secondary_label Output]
2Type '{ log: (message: string) => void; otherProp: boolean; }' is not assignable to type 'Logger'.
3  Object literal may only specify known properties, and 'otherProp' does not exist in type 'Logger'. (2322)

类似于使用正常的类型声明,可以将属性转换为可选属性,将?附加到其名称中。

扩展其他类型

在创建接口时,您可以从不同的对象类型扩展,允许您的接口包含来自扩展类型的所有类型信息。

假设你有一个可清晰的界面,如这个:

1interface Clearable {
2  clear: () => void;
3}

然后,你可以创建一个新的界面,延伸到它,继承所有的字段。在下面的示例中,界面Logger正在延伸到Clearable界面。

1interface Clearable {
2  clear: () => void;
3}
4
5interface Logger extends Clearable {
6  log: (message: string) => void;
7}

Logger界面现在也有一个清晰会员,这是一个不接受参数并返回的函数。

1interface Logger {
2  log: (message: string) => void;
3  clear: () => void;
4}

当使用一组共同的字段编写大量界面时,您可以将其提取到不同的界面,并更改界面,以扩展到您创建的新界面。

回到以前使用的可清除示例,想象你的应用程序需要一个不同的接口,例如下面的StringList接口,以表示包含多个字符串的数据结构:

1interface StringList {
2  push: (value: string) => void;
3  get: () => string[];
4}

通过使这个新的StringList界面扩展现有的Clearable界面,您正在指定该界面还具有Clearable界面中的成员,将Clear属性添加到StringList界面的类型定义中:

1interface StringList extends Clearable {
2  push: (value: string) => void;
3  get: () => string[];
4}

接口可以从任何对象类型扩展,如接口,正常类型,甚至是

具有可召唤性签名的接口

如果接口也是可调用的(即它也是一个函数),则可以通过创建可调用的签名在接口声明中传输该信息。

可以调用的签名是通过在接口中添加一个不关联到任何成员的函数声明,并在设置函数返回类型时使用 :而不是 `=>'来创建的。

例如,您可以将可调用的签名添加到您的Logger接口中,如下列突出代码中所示:

1interface Logger {
2  (message: string): void;
3  log: (message: string) => void;
4}

请注意,可调用的签名类似于匿名函数的类型声明,但在返回类型中,您使用的是 : 而不是 =>

要创建与您的Logger接口相匹配的值,您需要考虑接口的要求:

它必须具有一个名为log的属性,即一个接受单个 string参数的函数。

让我们创建一个名为logger的变量,可以分配到您的logger界面类型:

 1interface Logger {
 2  (message: string): void;
 3  log: (message: string) => void;
 4}
 5
 6const logger: Logger = (message: string) => {
 7  console.log(message);
 8}
 9logger.log = (message: string) => {
10  console.log(message);
11}

要匹配Logger界面,该值必须可调用,这就是为什么您将logger变量分配给函数的原因:

 1interface Logger {
 2  (message: string): void;
 3  log: (message: string) => void;
 4}
 5
 6const logger: Logger = (message: string) => {
 7  console.log(message);
 8}
 9logger.log = (message: string) => {
10  console.log(message);
11}

然后您将日志属性添加到日志函数中:

 1interface Logger {
 2  (message: string): void;
 3  log: (message: string) => void;
 4}
 5
 6const logger: Logger = (message: string) => {
 7  console.log(message);
 8}
 9logger.log = (message: string) => {
10  console.log(message);
11}

Logger接口相关的值也必须具有一个log属性,该属性是接受单个字符串参数并返回的函数。

如果你没有包含日志属性,TypeScript 编译器会给你错误2741:

1[secondary_label Output]
2Property 'log' is missing in type '(message: string) => void' but required in type 'Logger'. (2741)

TypeScript 编译器会发出类似的错误,如果日志变量中的日志属性具有不兼容的类型签名,例如将其设置为:

1interface Logger {
2  (message: string): void;
3  log: (message: string) => void;
4}
5
6const logger: Logger = (message: string) => {
7  console.log(message);
8}
9logger.log = true;

在这种情况下,TypeScript 编译器会显示错误 2322:

1[secondary_label Output]
2Type 'boolean' is not assignable to type '(message: string) => void'. (2322)

设置变量具有特定类型的一个好特征,在这种情况下,将日志变量设置为日志接口的类型,是TypeScript现在可以推断日志函数和日志属性中的函数的参数类型。

您可以通过从两个函数的参数中删除类型信息来验证这一点,请注意在下面的突出代码中,消息参数没有类型:

 1interface Logger {
 2  (message: string): void;
 3  log: (message: string) => void;
 4}
 5
 6const logger: Logger = (message) => {
 7  console.log(message);
 8}
 9logger.log = (message) => {
10  console.log(message);
11}

在这两种情况下,您的编辑器仍然可以显示参数的类型为字符串,因为这是日志器界面所期望的类型。

使用索引签名的界面

您可以将索引签名添加到您的界面,就像正常类型一样,从而允许界面具有无限数量的属性。

例如,如果您想要创建具有无限数量的字符串字段的DataRecord接口,则可以使用以下突出索引签名:

1interface DataRecord {
2  [key: string]: string;
3}

然后,您可以使用DataRecord界面来设置具有多个类型的字符串参数的任何对象的类型:

 1interface DataRecord {
 2  [key: string]: string;
 3}
 4
 5const data: DataRecord = {
 6  fieldA: "valueA",
 7  fieldB: "valueB",
 8  fieldC: "valueC",
 9  // ...
10};

在本节中,您使用TypeScript中可用的不同功能创建了界面,并了解如何使用您创建的界面。在下一节中,您将了解有关类型界面声明之间的差异,并获得声明合并和模块增加的实践。

类型和界面之间的区别

到目前为止,您已经看到界面声明和类型声明是相似的,几乎具有相同的功能。

例如,您创建了一个从可清除接口扩展的日志接口:

1interface Clearable {
2  clear: () => void;
3}
4
5interface Logger extends Clearable {
6  log: (message: string) => void;
7}

同一类型表示可以通过使用两个类型声明复制:

1type Clearable = {
2  clear: () => void;
3}
4
5type Logger = Clearable & {
6  log: (message: string) => void;
7}

如前面所示,界面声明可以用来表示各种对象,从函数到具有无限属性的复杂对象。

由于类型声明和界面声明如此相似,您需要考虑每个特征的独特性,并在您的代码库中保持一致性。

例如,类型声明具有界面声明缺乏的几个特性,例如:

  • 联盟类型.
  • 地图类型.
  • 向原始类型代名。

只有在界面声明中可用的功能之一是声明合并,您将在下一节中了解这一点,重要的是要注意,如果您正在编写库,并且想要让库用户能够扩展图书馆提供的类型,则声明合并可能有用,因为在类型声明中是不可能的。

合并声明

TypeScript 可以将多个声明合并为一个,使您能够为相同的数据结构编写多个声明,并在编译过程中由 TypeScript 编译器组合它们,仿佛它们是单个类型。

TypeScript 中的接口可以重新打开,即可以合并相同接口的多个声明。

例如,假设您有一个名为数据库选项的界面,如下:

1interface DatabaseOptions {
2  host: string;
3  port: number;
4  user: string;
5  password: string;
6}

此接口将用于在连接到数据库时传输选项。

后来在代码中,您声明一个具有相同名称但具有单个字符串字段的界面,称为dsnUrl,如下:

1interface DatabaseOptions {
2  dsnUrl: string;
3}

当TypeScript 编译器开始读取您的代码时,它将将DatabaseOptions界面的所有声明合并为一个。

1interface DatabaseOptions {
2  host: string;
3  port: number;
4  user: string;
5  password: string;
6  dsnUrl: string;
7}

界面包括您最初声明的所有字段,加上您单独声明的新字段 `dsnUrl。

模块增加

当您需要将现有模块增加到新的属性时,声明合并是有用的。其中一个用例是当您将更多字段添加到库提供的数据结构时。

在使用表达式时,将一个请求和一个响应对象传递给您的请求处理器(负责提供对 HTTP 请求的响应功能)。

1const myRoute = (req: Request, res: Response) => {
2  res.json({ user: req.user });
3}

在这里,请求处理器将一个json发送回客户端,并将用户字段设置为已登录的用户。

请求界面本身的类型定义没有一个用户字段,所以上面的代码会给出类型错误2339:

1Property 'user' does not exist on type 'Request'. (2339)

要修复此问题,您必须为快递包创建一个模块扩展,利用声明合并将新属性添加到请求界面。

如果您在表达式类型声明中检查请求对象的类型,您会注意到它是在名为表达式的全球名称空间中添加的接口,如从 DefinitelyTyped repository的文档中所示:

1declare global {
2    namespace Express {
3        // These open interfaces may be extended in an application-specific manner via declaration merging.
4        // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
5        interface Request {}
6        interface Response {}
7        interface Application {}
8    }
9}

<$>[注] 注: 类型声明文件是只包含类型信息的文件。 DefinitelyTyped 存储库是向没有类型的包提交类型声明的官方存储库。

若要使用模块扩展来将新属性添加到请求界面,则必须在本地类型声明文件中复制相同的结构,例如,假设您创建了一个名为express.d.ts的文件,如下,然后将其添加到您的tsconfig.json类型选项中:

 1import 'express';
 2
 3declare global {
 4  namespace Express {
 5    interface Request {
 6      user: {
 7        name: string;
 8      }
 9    }
10  }
11}

从TypeScript 编译器的观点来看,请求界面有一个用户属性,其类型设置为一个具有单一属性的对象,称为名称的类型字符串

假设您正在创建一个库,并希望为您的库的用户提供增加您自己的库提供的类型的选项,就像您在表达上所做的那样。

结论

在本教程中,您已经编写了多种TypeScript界面来代表各种数据结构,发现如何将不同的界面一起作为构建块来创建强大的类型,并了解了正常类型声明和界面之间的差异。

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

Published At
Categories with 技术
comments powered by Disqus