介绍
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.2 版创建的。
在 TypeScript 中创建和使用接口
在本节中,我们将使用 TypeScript 中可用的不同功能创建接口,您还将学习如何使用您创建的接口。
TypeScript 中的接口是通过使用 interface 关键字后跟接口名称,然后是带有接口主体的 {} 块来创建的。例如,这里是一个 Logger 接口:
interface Logger {
log: (message: string) => void;
}
与使用类型声明创建普通类型类似,我们可以在 {} 中指定类型的字段及其类型:
interface Logger {
log: (message: string) => void;
}
Logger 接口表示一个对象,该对象具有一个名为 log 的属性。此属性是一个接受字符串类型的单个参数并返回 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 接口声明中不存在此属性:
Output
Type '{ 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)
与使用普通类型声明类似,可以通过附加 ? 将属性转换为可选属性。以他们的名义。
扩展其他类型
创建接口时,我们可以从不同的对象类型进行扩展,允许您的接口包含来自扩展类型的所有类型信息。这使您能够编写具有一组通用字段的小型接口,并将它们用作构建块来创建新接口。
想象一下,我们如果有一个 Clearable 接口,比如这个:
interface Clearable {
clear: () => void;
}
然后,我们可以创建一个从它扩展的新接口,继承它的所有字段。在以下示例中,接口 Logger 是从 Clearable 接口扩展而来的。注意突出显示的行:
xxxxxxxxxx
interface Clearable {
clear: () => void;
}
interface Loggerextends Clearable{
log: (message: string) => void;
}
Logger 接口现在还有一个 clear 成员,它是一个不接受参数并返回 void 的函数。这个新成员继承自 Clearable 接口。就像我们这样做一样:
xxxxxxxxxx
interface Logger {
log: (message: string) => void;
clear: () => void;
}
当使用一组通用字段编写大量接口时,我们可以将它们提取到不同的接口并更改接口以扩展创建的新接口。
回到前面使用的 Clearable 示例,假设我们的应用程序需要一个不同的接口,例如下面的 StringList 接口,来表示一个包含多个字符串的数据结构:
interface StringList {
push: (value: string) => void;
get: () => string[];
}
通过使这个新的 StringList 接口扩展现有的 Clearable 接口,指定该接口还具有在 Clearable 接口中设置的成员,将 clear 属性添加到 StringList 接口的类型定义中:
interface StringList extends Clearable {
push: (value: string) => void;
get: () => string[];
}
接口可以从任何对象类型扩展,例如接口、普通类型,甚至是类。
带有可调用签名的接口
如果接口也是可调用的(也就是说,它也是一个函数),我们可以通过创建可调用签名在接口声明中传达该信息。
通过在未绑定到任何成员的接口内添加函数声明并在设置函数的返回类型时使用 : 而不是 => 来创建可调用签名。
例如,在 Logger 界面中添加一个可调用的签名,如下面突出显示的代码所示:
interface Logger {
(message: string): void;
log: (message: string) => void;
}
请注意,可调用签名类似于匿名函数的类型声明,但在返回类型中,我们使用的是 : 而不是 =>。这意味着绑定到 Logger 接口类型的任何值都可以作为函数直接调用。
要创建与Logger 接口匹配的值,我们需要考虑接口的要求:
- 它必须是可调用的。
- 它必须有一个名为 log 的属性,该属性是一个接受单个字符串参数的函数。
让我们创建一个名为 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 属性,该属性是一个接受单个字符串参数并返回 void 的函数。
如果我们没有包含 log 属性,TypeScript Compiler 会给你错误 2741:
Output
Property '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:
Output
Type 'boolean' is not assignable to type '(message: string) => void'. (2322)
将变量设置为具有特定类型的一个很好的功能,在这种情况下,将记录器变量设置为具有记录器接口的类型,TypeScript 现在可以推断记录器函数和日志中函数的参数类型 财产。
我们可以通过从两个函数的参数中删除类型信息来检查。请注意,在下面突出显示的代码中,消息参数没有类型:
interface Logger {
(message: string): void;
log: (message: string) => void;
}
const logger: Logger = (message)=> {
console.log(message);
}
logger.log = (message)=> {
console.log(message);
}
在这两种情况下,编辑器应该仍然能够显示参数的类型是字符串,因为这是 Logger 接口所期望的类型。
带有索引签名的接口
可以向界面添加索引签名,就像使用普通类型一样,从而允许界面具有无限数量的属性。
例如,如果想创建一个具有无限数量的字符串字段的 DataRecord 接口,可以使用以下突出显示的索引签名:
interface DataRecord {
[key: string]: string;
}
然后,我们可以使用 DataRecord 接口设置具有多个字符串类型参数的任何对象的类型:
interface DataRecord {
[key: string]: string;
}
const data: DataRecord = {
fieldA: "valueA",
fieldB: "valueB",
fieldC: "valueC",
// ...
};
在本文中,我们使用 TypeScript 中可用的不同功能创建了接口,并学习了如何使用您创建的接口。
在接下来的内容中,我们将了解更多关于类型和接口声明之间的区别,并获得声明合并和模块扩充的实践。
类型和接口的区别
到目前为止,我们已经看到接口声明和类型声明是相似的,具有几乎相同的特性集。
例如,我们创建了一个从 Clearable 接口扩展而来的 Logger 接口:
interface Clearable {
clear: () => void;
}
interface Logger extends Clearable {
log: (message: string) => void;
}
可以使用两种类型声明来复制相同的类型表示:
type Clearable = {
clear: () => void;
}
type Logger = Clearable & {
log: (message: string) => void;
}
如前面内容所示,接口声明可用于表示各种对象,从函数到具有无限数量属性的复杂对象。这也适用于类型声明,甚至从其他类型扩展,因为,我们可以使用交集运算符 & 将多个类型相交。
由于类型声明和接口声明非常相似,因此,需要考虑各自独有的特定功能,并在代码库中保持一致。选择一种在代码库中创建类型表示,并且仅在需要仅对它可用的特定功能时才使用另一种。
例如,类型声明具有接口声明所缺乏的一些特性,例如:
- 联合类型。
- 映射类型。
- 原始类型的别名。
仅可用于接口声明的功能之一是声明合并,我们将在接下来的内容中学习它。重要的是要注意,如果您正在编写一个库并希望为库用户提供扩展库提供的类型的能力,那么声明合并可能很有用,因为类型声明无法做到这一点。
声明合并
TypeScript 可以将多个声明合并为一个声明,使他们能够为同一个数据结构编写多个声明,并在编译期间将它们捆绑在一起,就像它们是一个单一类型一样。
在文中,我们将看到它是如何工作的,以及为什么它在使用接口时很有帮助。
TypeScript 中的接口可以重新打开;也就是说,可以合并同一接口的多个声明。当我们想要将新字段添加到现有界面时,这很有用。
例如,假设我们有一个名为 DatabaseOptions 的接口,如下所示:
interface DatabaseOptions {
host: string;
port: number;
user: string;
password: string;
}
此接口将用于在连接到数据库时传递选项。
稍后在代码中,声明一个具有相同名称但具有一个名为 dsnUrl 的字符串字段的接口,如下所示
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 发送回客户端。使用负责用户身份验证的快速中间件,将登录的用户添加到代码中另一个位置的请求对象。
Request 接口本身的类型定义没有用户字段,因此上面的代码会给出类型错误 2339:
Property 'user' does not exist on type 'Request'. (2339)
要解决这个问题,我们必须为 express 包创建一个模块扩充,利用声明合并向请求接口添加一个新属性。
如果我们在 express 类型声明中检查 Request 对象的类型,我们会注意到它是一个添加在名为 Express 的全局命名空间中的接口,如 DefinitiveTyped 存储库中的文档所示:
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 接口有一个用户属性,它们的类型设置为一个对象,该对象具有一个称为字符串类型名称的属性。发生这种情况是因为同一接口的所有声明都被合并了。
假设我们正在创建一个库,并希望为我们的库的用户提供增加自己的库提供的类型的选项,就像我们在上面使用 express 所做的那样。在这种情况下,我们需要从库中导出接口,因为普通类型声明不支持模块扩充。
结论
到这里,在本文提供的教程就结束了。
我们编写了多个 TypeScript 接口来表示各种数据结构,发现了如何将不同的接口一起用作构建块来创建强大的类型,并了解了普通类型声明和接口之间的区别。
我们现在可以开始为代码库中的数据结构编写接口,让我们拥有类型安全的代码和文档。