如何在TypeScript中使用接口
作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
TypeScript 是 JavaScript 语言的扩展,它使用带有编译时类型检查器的 JavaScript 运行时。
TypeScript 提供了多种方法来表示代码中的对象,其中一种是使用接口。 TypeScript 中的接口有两种使用场景:您可以创建类必须遵循的协定,例如那些类必须实现的成员,您还可以在应用程序中表示类型,就像普通的 type
声明一样。 (有关 types
的更多信息,请查看 如何在 TypeScript 中使用基本类型和 如何在 TypeScript 中创建自定义类型。)
您可能会注意到接口和类型共享一组相似的功能。 事实上,一个几乎总是可以替代另一个。 主要区别在于接口可能对同一个接口有多个声明,TypeScript 将合并这些声明,而类型只能声明一次。 您还可以使用类型来创建原始类型的别名(例如 string
和 boolean
),这是接口无法做到的。
TypeScript 中的接口是表示类型结构的强大方法。 它们允许您以类型安全的方式使用这些结构并同时记录它们,从而直接改善开发人员体验。
在本教程中,您将在 TypeScript 中创建接口,学习如何使用它们,并了解普通类型和接口之间的区别。 您将尝试不同的代码示例,您可以在自己的 TypeScript 环境或 TypeScript Playground(一个允许您直接在浏览器中编写 TypeScript 的在线环境)中进行操作。
先决条件
要遵循本教程,您将需要:
- 一个环境,您可以在其中执行 TypeScript 程序以跟随示例。 要在本地计算机上进行设置,您将需要以下内容。
- 安装了 Node 和 npm(或 yarn)以运行处理 TypeScript 相关包的开发环境。 本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 进行了测试。 要在 macOS 或 Ubuntu 18.04 上安装,请按照 如何在 macOS 上安装 Node.js 和创建本地开发环境中的步骤或 How 的 使用 PPA 部分安装在 Ubuntu 18.04 上安装 Node.js。 如果您使用的是 Windows Subsystem for Linux (WSL),这也适用。
- 此外,您需要在您的机器上安装 TypeScript 编译器 (
tsc
)。 为此,请参阅 官方 TypeScript 网站 。
- 如果您不想在本地机器上创建 TypeScript 环境,您可以使用官方的 TypeScript Playground 进行操作。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如 解构、剩余运算符 和 导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列。
- 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。 您还可以在 TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
在 TypeScript 中创建和使用接口
在本节中,您将使用 TypeScript 中可用的不同功能创建接口。 您还将学习如何使用您创建的接口。
TypeScript 中的接口是通过使用 interface
关键字后跟接口名称,然后是带有接口主体的 {}
块来创建的。 例如,这里是一个 Logger
接口:
interface Logger { log: (message: string) => void; }
与使用 type
声明创建普通类型类似,您可以在 {}
中指定类型的字段及其类型:
interface Logger { log: (message: string) => void; }
Logger
接口表示一个对象,它有一个名为 log
的属性。 此属性是一个函数,它接受 string
类型的单个参数并返回 void
。
您可以将 Logger
接口用作任何其他类型。 下面是一个创建与 Logger
接口匹配的对象字面量的示例:
interface Logger { log: (message: string) => void; } const logger: Logger = { log: (message) => console.log(message), };
使用 Logger
接口作为其类型的值必须具有与 Logger
接口声明中指定的成员相同的成员。 如果某些成员是可选的,则可以省略它们。
由于值必须遵循接口中声明的内容,因此添加无关字段将导致编译错误。 例如,在对象字面量中,尝试添加接口中缺少的新属性:
interface Logger { log: (message: string) => void; } const logger: Logger = { log: (message) => console.log(message), otherProp: true, };
在这种情况下,TypeScript 编译器会发出错误 2322
,因为 Logger
接口声明中不存在此属性:
OutputType '{ log: (message: string) => void; otherProp: boolean; }' is not assignable to type 'Logger'. Object literal may only specify known properties, and 'otherProp' does not exist in type 'Logger'. (2322)
与使用普通的 type
声明类似,可以通过将 ?
附加到属性名称来将属性转换为可选属性。
扩展其他类型
创建接口时,您可以从不同的对象类型进行扩展,允许您的接口包含来自扩展类型的所有类型信息。 这使您能够编写具有一组通用字段的小型接口,并将它们用作构建块来创建新接口。
假设你有一个 Clearable
接口,比如这个:
interface Clearable { clear: () => void; }
然后,您可以创建一个从它扩展的新接口,继承它的所有字段。 在以下示例中,接口 Logger
是从 Clearable
接口扩展而来的。 注意突出显示的行:
interface Clearable { clear: () => void; } interface Logger extends Clearable { log: (message: string) => void; }
Logger
接口现在还有一个 clear
成员,这是一个不接受参数并返回 void
的函数。 这个新成员继承自 Clearable
接口。 就像我们这样做一样:
interface Logger { log: (message: string) => void; clear: () => void; }
当使用一组通用字段编写大量接口时,您可以将它们提取到不同的接口并更改您的接口以扩展您创建的新接口。
回到前面使用的 Clearable
示例,假设您的应用程序需要一个不同的接口,例如下面的 StringList
接口,来表示一个包含多个字符串的数据结构:
interface StringList { push: (value: string) => void; get: () => string[]; }
通过使这个新的 StringList
接口扩展现有的 Clearable
接口,您指定此接口还具有在 Clearable
接口中设置的成员,添加 [X187X ] 属性添加到 StringList
接口的类型定义中:
interface StringList extends Clearable { push: (value: string) => void; get: () => string[]; }
接口可以从任何对象类型扩展,例如接口、普通类型,甚至 类。
带有可调用签名的接口
如果接口也是可调用的(也就是说,它也是一个函数),您可以通过创建可调用签名在接口声明中传达该信息。
通过在未绑定到任何成员的接口内添加函数声明并在设置函数的返回类型时使用 :
而不是 =>
来创建可调用签名。
例如,向您的 Logger
接口添加一个可调用签名,如下面突出显示的代码所示:
interface Logger { (message: string): void; log: (message: string) => void; }
请注意,可调用签名类似于匿名函数的类型声明,但在返回类型中您使用的是 :
而不是 =>
。 这意味着绑定到 Logger
接口类型的任何值都可以作为函数直接调用。
要创建与您的 Logger
接口匹配的值,您需要考虑接口的要求:
- 它必须是可调用的。
- 它必须有一个名为
log
的属性,它是一个接受单个string
参数的函数。
让我们创建一个名为 logger
的变量,它可以分配给 Logger
接口的类型:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = (message: string) => { console.log(message); }
要匹配 Logger
接口,值必须是可调用的,这就是为什么将 logger
变量分配给函数的原因:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = (message: string) => { console.log(message); }
然后将 log
属性添加到 logger
函数:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = (message: string) => { console.log(message); }
这是 Logger
接口所要求的。 绑定到 Logger
接口的值还必须具有 log
属性,该属性是接受单个 string
参数并返回 void
的函数。
如果你没有包含 log
属性,TypeScript 编译器会给你错误 2741
:
OutputProperty 'log' is missing in type '(message: string) => void' but required in type 'Logger'. (2741)
如果 logger
变量中的 log
属性具有不兼容的类型签名,TypeScript 编译器将发出类似的错误,例如将其设置为 true
:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message: string) => { console.log(message); } logger.log = true;
在这种情况下,TypeScript 编译器会显示错误 2322
:
OutputType 'boolean' is not assignable to type '(message: string) => void'. (2322)
将变量设置为具有特定类型的一个很好的功能,在这种情况下,将 logger
变量设置为具有 Logger
接口的类型,TypeScript 现在可以推断参数的类型logger
函数和 log
属性中的函数。
您可以通过从两个函数的参数中删除类型信息来检查。 请注意,在下面突出显示的代码中,message
参数没有类型:
interface Logger { (message: string): void; log: (message: string) => void; } const logger: Logger = (message) => { console.log(message); } logger.log = (message) => { console.log(message); }
在这两种情况下,您的编辑器应该仍然能够显示参数的类型是 string
,因为这是 Logger
接口所期望的类型。
带有索引签名的接口
您可以向界面添加索引签名,就像使用普通类型一样,从而允许界面具有无限数量的属性。
例如,如果您想创建一个具有无限数量的 string
字段的 DataRecord
接口,您可以使用以下突出显示的索引签名:
interface DataRecord { [key: string]: string; }
然后,您可以使用 DataRecord
接口设置具有多个 string
类型参数的任何对象的类型:
interface DataRecord { [key: string]: string; } const data: DataRecord = { fieldA: "valueA", fieldB: "valueB", fieldC: "valueC", // ... };
在本节中,您使用 TypeScript 中可用的不同功能创建了接口,并学习了如何使用您创建的接口。 在下一节中,您将详细了解 type
和 interface
声明之间的区别,并练习声明合并和模块扩充。
类型和接口的区别
到目前为止,您已经看到 interface
声明和 type
声明是相似的,具有几乎相同的特性集。
例如,您创建了一个从 Clearable
接口扩展而来的 Logger
接口:
interface Clearable { clear: () => void; } interface Logger extends Clearable { log: (message: string) => void; }
可以使用两个 type
声明来复制相同的类型表示:
type Clearable = { clear: () => void; } type Logger = Clearable & { log: (message: string) => void; }
如前几节所示,interface
声明可用于表示各种对象,从函数到具有无限数量属性的复杂对象。 这也可以使用 type
声明,甚至从其他类型扩展,因为您可以使用交集运算符 &
将多个类型相交。
由于 type
声明和 interface
声明非常相似,因此您需要考虑每个声明独有的特定功能,并在您的代码库中保持一致。 选择一种在您的代码库中创建类型表示,并且仅在您需要仅对它可用的特定功能时才使用另一种。
例如,type
声明具有 interface
声明所缺乏的一些特性,例如:
- 联合类型。
- 映射类型。
- 原始类型的别名。
仅可用于 interface
声明的功能之一是声明合并,您将在下一节中了解它。 重要的是要注意,如果您正在编写一个库并希望让库用户能够扩展库提供的类型,那么声明合并可能很有用,因为 type
声明无法做到这一点。
声明合并
TypeScript 可以将多个声明合并为一个声明,使您能够为同一个数据结构编写多个声明,并在编译期间将它们捆绑在一起,就像它们是单个类型一样。 在本节中,您将看到它是如何工作的,以及为什么它在使用接口时很有帮助。
TypeScript 中的接口可以重新打开; 也就是说,可以合并同一接口的多个声明。 当您想要将新字段添加到现有界面时,这很有用。
例如,假设您有一个名为 DatabaseOptions
的接口,如下所示:
interface DatabaseOptions { host: string; port: number; user: string; password: string; }
此接口将用于在连接到数据库时传递选项。
稍后在代码中,您声明了一个同名的接口,但有一个名为 dsnUrl
的 string
字段,如下所示:
interface DatabaseOptions { dsnUrl: string; }
当 TypeScript 编译器开始读取您的代码时,它会将 DatabaseOptions
接口的所有声明合并为一个。 从 TypeScript 编译器的角度来看,DatabaseOptions
现在是:
interface DatabaseOptions { host: string; port: number; user: string; password: string; dsnUrl: string; }
该接口包括您最初声明的所有字段,以及您单独声明的新字段 dsnUrl
。 两个声明已合并。
模块扩充
当您需要使用新属性扩充现有模块时,声明合并很有帮助。 一个用例是当您向库提供的数据结构添加更多字段时。 这在名为 express 的 Node.js 库中相对常见,它允许您创建 HTTP 服务器。
当使用 express
时,一个 Request
和一个 Response
对象被传递给您的请求处理程序(负责为 HTTP 请求提供响应的函数)。 Request
对象通常用于存储特定于特定请求的数据。 例如,您可以使用它来存储发出初始 HTTP 请求的登录用户:
const myRoute = (req: Request, res: Response) => { res.json({ user: req.user }); }
在这里,请求处理程序将 json
发送回客户端,其中 user
字段设置为登录用户。 使用负责用户身份验证的快速中间件,将登录的用户添加到代码中另一个位置的请求对象。
Request
接口的类型定义本身没有 user
字段,因此上面的代码会给出类型错误 2339
:
Property 'user' does not exist on type 'Request'. (2339)
要解决这个问题,您必须为 express
包创建一个模块扩充,利用声明合并向 Request
接口添加新属性。
如果您在 express
类型声明中检查 Request
对象的类型,您会注意到它是一个添加在名为 Express
的全局命名空间中的接口,如文档所示来自 DefinitelyTyped 存储库:
declare global { namespace Express { // These open interfaces may be extended in an application-specific manner via declaration merging. // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts) interface Request {} interface Response {} interface Application {} } }
注意: 类型声明文件是只包含类型信息的文件。 DefinitiveTyped 存储库是为没有类型声明的包提交类型声明的官方存储库。 npm 上可用的 @types/<package>
包是从此存储库发布的。
要使用模块扩充向 Request
接口添加新属性,您必须在本地类型声明文件中复制相同的结构。 例如,假设您创建了一个名为 express.d.ts
的文件,如下所示,然后将其添加到 tsconfig.json
的 types
选项中:
import 'express'; declare global { namespace Express { interface Request { user: { name: string; } } } }
从 TypeScript 编译器的角度来看,Request
接口有一个 user
属性,它们的类型设置为一个对象,该对象具有一个名为 name
的单一属性,类型为 [ X181X]。 发生这种情况是因为同一接口的所有声明都被合并了。
假设您正在创建一个库,并希望为您的库的用户提供增加您自己的库提供的类型的选项,就像您在上面使用 express
所做的那样。 在这种情况下,您需要从库中导出接口,因为正常的 type
声明不支持模块扩充。
结论
在本教程中,您编写了多个 TypeScript 接口来表示各种数据结构,发现了如何将不同的接口一起用作构建块来创建强大的类型,并了解了普通类型声明和接口之间的区别。 您现在可以开始为代码库中的数据结构编写接口,让您拥有类型安全的代码和文档。
有关 TypeScript 的更多教程,请查看我们的 How To Code in TypeScript 系列页面。