TypeScript 是 JavaScript 语言的扩展,它使用 JavaScript 运行时和编译时类型检查器。TypeScript 提供了多种方法来表示代码中的对象,其中一种是使用接口。 TypeScript 中的接口有两种使用场景:您可以创建类必须遵循的约定,例如,这些类必须实现的成员,还可以在应用程序中表示类型,就像普通的类型声明一样。
在今天的文章中,我们将在 TypeScript 中创建接口,学习如何使用它们,并了解普通类型和接口之间的区别。
我们将尝试不同的代码示例,可以在 TypeScript 环境或 TypeScript Playground(一个允许您直接在浏览器中编写 TypeScript 的在线环境)中遵循这些示例。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
namespace DatabaseEntity { }这声明了 DatabaseEntity 命名空间,但尚未向该命名空间添加代码。 接下来,在命名空间中添加一个 User 类来表示数据库中的一个 User 实体:
namespace DatabaseEntity { class User { constructor(public name: string) {} } }我们可以在命名空间中正常使用 User 类。 为了说明这一点,创建一个新的 User 实例并将其存储在 newUser 变量中:
namespace DatabaseEntity { class User { constructor(public name: string) {} } const newUser = new User("Jon"); }这是有效的代码。 但是,如果我们尝试在命名空间之外使用 User,TypeScript 编译器会给我们返回错误 2339:
Output Property 'User' does not exist on type 'typeof DatabaseEntity'. (2339)如果我们想在命名空间之外使用类,则必须首先导出 User 类以在外部可用,如下面突出显示的代码所示:
namespace DatabaseEntity { exportclass User { constructor(public name: string) {} } const newUser = new User("Jon"); }我们现在可以使用完全限定名称访问 DatabaseEntity 命名空间之外的 User 类。 在这种情况下,完全限定名称是 DatabaseEntity.User:
namespace DatabaseEntity { export class User { constructor(public name: string) {} } const newUser = new User("Jon"); } const newUserOutsideNamespace = new DatabaseEntity.User("Jane");我们可以从命名空间中导出任何内容,包括变量,然后这些变量将成为命名空间中的属性。 在以下代码中,我们将导出 newUser 变量:
namespace DatabaseEntity { export class User { constructor(public name: string) {} } exportconst newUser = new User("Jon"); } console.log(DatabaseEntity.newUser.name);由于变量 newUser 已导出,因此,我们可以将其作为命名空间的属性进行访问。 运行此代码会将以下内容打印到控制台:
Output Jon就像接口一样,TypeScript 中的命名空间也允许声明合并。 这意味着同一命名空间的多个声明将合并为一个声明。 如果我们需要稍后在代码中扩展命名空间,这可以增加命名空间的灵活性。
namespace DatabaseEntity { export class User { constructor(public name: string) {} } export const newUser = new User("Jon"); } namespace DatabaseEntity { export class UserRole { constructor(public user: User, public role: string) {} } export const newUserRole = new UserRole(newUser, "admin"); }在新的 DatabaseEntity 命名空间声明中,我们可以使用以前在 DatabaseEntity 命名空间中导出的任何成员,包括从以前的声明中导出的成员,而不必使用它们的完全限定名。我们正在使用在第一个命名空间中声明的名称来将 UserRole 构造函数中的用户参数的类型设置为 User 类型,并在使用 newUser 值创建新的 UserRole 实例时。这仅是可能的,因为,我们在之前的命名空间声明中导出了这些内容。
namespace DatabaseEntity { export class User { constructor(public name: string) {} } export const newUser = new User("Jon"); } console.log(DatabaseEntity.newUser.name);TypeScript 编译器会为此 TypeScript 片段生成以下 JavaScript 代码:
"use strict"; var DatabaseEntity; (function (DatabaseEntity) { class User { constructor(name) { this.name = name; } } DatabaseEntity.User = User; DatabaseEntity.newUser = new User("Jon"); })(DatabaseEntity || (DatabaseEntity = {})); console.log(DatabaseEntity.newUser.name);为了声明 DatabaseEntity 命名空间,TypeScript 编译器创建一个名为 DatabaseEntity 的未初始化变量,然后,创建一个立即调用函数表达式 (IIFE)。 此 IIFE 接收单个参数 DatabaseEntity || (DatabaseEntity = {}),这是 DatabaseEntity 变量的当前值。 如果未设置为真值,则将变量的值设置为空对象。在将 DatabaseEntity 的值传递给 IIFE 时将其设置为空值是可行的,因为赋值操作的返回值是被赋值的值。 在这种情况下,这是空对象。
namespace DatabaseEntity { export class User { constructor(public name: string) {} } export const newUser = new User("Jon"); } namespace DatabaseEntity { export class UserRole { constructor(public user: User, public role: string) {} } export const newUserRole = new UserRole(newUser, "admin"); }生成的 JavaScript 代码如下所示:
"use strict"; var DatabaseEntity; (function (DatabaseEntity) { class User { constructor(name) { this.name = name; } } DatabaseEntity.User = User; DatabaseEntity.newUser = new User("Jon"); })(DatabaseEntity || (DatabaseEntity = {})); (function (DatabaseEntity) { class UserRole { constructor(user, role) { this.user = user; this.role = role; } } DatabaseEntity.UserRole = UserRole; DatabaseEntity.newUserRole = new UserRole(DatabaseEntity.newUser, "admin"); })(DatabaseEntity || (DatabaseEntity = {}));代码的开头看起来与之前的相同,未初始化的变量 DatabaseEntity,然后是一个 IIFE,其中实际代码设置了 DatabaseEntity 对象的属性。这一次,虽然,还有另一个 IIFE。这个新的 IIFE 与 DatabaseEntity 命名空间的第二个声明相匹配。现在,当执行第二个 IIFE 时,DatabaseEntity 已经绑定到一个对象,因此,我们只是通过添加额外属性来扩展已经可用的对象。
export class Vector3 { super(x, y, z) { this.x = x; this.y = y; this.z = z; } add(vec) { let x = this.x + vector.x; let y = this.y + vector.y; let z = this.z + vector.z; let newVector = new Vector3(x, y, z); return newVector } }这导出了一个类,该类创建具有 x、y 和 z 属性的向量,用于表示向量的坐标分量。接下来,看一下使用假设库的示例代码:
import { Vector3 } from "example-vector3"; const v1 = new Vector3(1, 2, 3); const v2 = new Vector3(1, 2, 3); const v3 = v1.add(v2);example-vector3 库没有与它自己的类型声明捆绑在一起,因此, TypeScript 编译器将给出错误 2307:
Output Cannot find module 'example-vector3' or its corresponding type declarations. ts(2307)为了解决这个问题,我们现在将为这个包创建一个类型声明文件。 首先,创建一个名为 types/example-vector3/index.d.ts 的新文件。然后,在您喜欢的编辑器中打开它。 在此文件中写入以下代码:
declare module "example-vector3" { export = vector3; namespace vector3 { } }在此代码中,我们正在为 example-vector3 模块创建类型声明。 代码的第一部分是声明模块块本身。 TypeScript 编译器将解析这个块并解释其中的所有内容,就好像它是模块本身的类型表示一样。 这意味着我们在此处声明的任何内容,TypeScript 都将用于推断模块的类型。 现在,您说这个模块导出了一个名为 vector3 的命名空间,该命名空间目前是空的。保存并退出此文件。TypeScript 编译器当前不知道您的声明文件,因此您必须将其包含在您的 tsconfig.json 中。
{ "compilerOptions": { ... "types": ["./types/example-vector3/index.d.ts"] } }现在,如果我们返回原始代码,我们将看到错误已更改。TypeScript 编译器现在给出错误是2305:
Output Module '"example-vector3"' has no exported member 'Vector3'. ts(2305)当我们为 example-vector3 创建模块声明时,导出当前设置为空命名空间。 没有从该命名空间中导出 Vector3 类。重新打开 types/example-vector3/index.d.ts 并编写以下代码:
declare module "example-vector3" { export = vector3; namespace vector3 { export class Vector3 { constructor(x: number, y: number, z: number); add(vec: Vector3): Vector3; } } }在此代码中,请注意,我们现在如何在 vector3 命名空间内导出一个类。模块声明的主要目标是提供由库公开的值的类型信息。这样,我们可以以类型安全的方式使用它。在这种情况下,我们知道 example-vector3 库提供了一个名为 Vector3 的类,该类在构造函数中接受三个数字,并且具有用于将两个 Vector3 实例相加的 add 方法,并返回一个新实例作为结果。