如何在TypeScript中使用命名空间

来自菜鸟教程
跳转至:导航、​搜索

作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

TypeScriptJavaScript 语言的扩展,它使用带有编译时类型检查器的 JavaScript 运行时。 在 TypeScript 中,您可以使用 命名空间 来组织您的代码。 TypeScript 中的命名空间以前称为内部模块,它基于 ECMAScript 模块的早期草案。 在 ECMAScript 规范草案中,内部模块在 September 2013 前后被删除,但 TypeScript 以不同的名称保留了这个想法。

命名空间允许开发人员创建可用于保存多个值的单独组织单元,例如属性、类型接口。 在本教程中,您将创建和使用命名空间来说明语法以及它们的用途。 它将引导您了解声明和合并命名空间的代码示例,命名空间如何在后台作为 JavaScript 代码工作,以及如何使用它们为外部库声明类型而无需键入。

先决条件

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

  • 一个环境,您可以在其中执行 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+ 语法,例如 解构、剩余运算符导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列
  • 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。 你也可以在 TypeScript Playground 中尝试这些好处。

本教程中显示的所有示例都是使用 TypeScript 4.2.3 版创建的。

在 TypeScript 中创建命名空间

在本节中,您将在 TypeScript 中创建命名空间以说明一般语法。

要创建命名空间,您将使用 namespace 关键字,后跟命名空间的名称,然后是 {} 块。 例如,您将创建一个 DatabaseEntity 命名空间来保存数据库实体,就好像您正在使用一个 对象关系映射 (ORM) 库一样。 将以下代码添加到新的 TypeScript 文件中:

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

OutputProperty 'User' does not exist on type 'typeof DatabaseEntity'. (2339)

如果您想在命名空间之外使用您的类,您必须首先导出 User 类以供外部使用,如下面突出显示的代码所示:

namespace DatabaseEntity {
  export class 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) {}
  }

  export const newUser = new User("Jon");
}

console.log(DatabaseEntity.newUser.name);

由于变量 newUser 已导出,因此您可以将其作为命名空间的属性进行访问。 运行此代码会将以下内容打印到控制台:

OutputJon

就像 interfaces 一样,TypeScript 中的命名空间也允许声明合并。 这意味着同一命名空间的多个声明将合并为一个声明。 如果您需要稍后在代码中扩展命名空间,这可以增加命名空间的灵活性。

使用前面的示例,这意味着如果您再次声明 DatabaseEntity 命名空间,您将能够使用更多属性扩展命名空间。 使用另一个命名空间声明将新类 UserRole 添加到您的 DatabaseEntity 命名空间:

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 参数的类型设置为 User 类型,并且在创建使用 newUser 值创建新的 UserRole 实例。 这仅是可能的,因为您在之前的命名空间声明中导出了这些内容。

现在您已经了解了命名空间的基本语法,您可以继续研究 TypeScript 编译器如何将命名空间转换为 JavaScript。

检查使用命名空间时生成的 JavaScript 代码

TypeScript 中的命名空间不仅仅是一个编译时特性。 他们还更改了生成的 JavaScript 代码。 要了解有关命名空间如何工作的更多信息,您可以分析支持此 TypeScript 功能的 JavaScript。 在这一步中,您将使用上一节中的代码片段并检查它们的底层 JavaScript 实现。

以您在第一个示例中使用的代码为例:

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 时将其设置为空值是可行的,因为赋值操作的返回值是被赋值的值。 在这种情况下,这是空对象。

在 IIFE 内部,创建了 User 类,然后将其分配给 DatabaseEntity 对象的 User 属性。 newUser 属性也是如此,您将属性分配给新的 User 实例的值。

现在看一下第二个代码示例,其中有多个命名空间声明:

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 已经绑定到一个对象,所以你只是通过添加额外的属性来扩展已经可用的对象。

您现在已经了解了 TypeScript 命名空间的语法以及它们在底层 JavaScript 中的工作方式。 有了这个上下文,您现在可以运行命名空间的一个常见用例:为外部库定义类型而无需键入。

使用命名空间为外部库提供类型

在本节中,您将体验命名空间有用的场景之一:为外部库创建模块声明。 为此,您将在 TypeScript 项目中编写一个新文件来声明类型,然后更改您的 tsconfig.json 文件以使 TypeScript 编译器识别类型。

注意: 要执行后续步骤,需要一个可以访问文件系统的 TypeScript 环境。 如果您使用的是 TypeScript Playground,您可以通过单击顶部菜单中的 Export,然后单击 在 CodeSandbox 中打开,将现有代码导出到 CodeSandbox 项目。 这将允许您创建新文件并编辑 tsconfig.json 文件。


并非 npm 注册表中可用的每个包都捆绑了自己的 TypeScript 模块声明。 这意味着在项目中安装包时,您可能会遇到与包缺少类型声明相关的编译错误,或者必须使用所有类型都设置为 any 的库。 根据您使用 TypeScript 的严格程度,这可能是一个不希望的结果。

希望这个包将有一个由 DefinetelyTyped 社区创建的 @types 包,允许您安装包并获得该库的工作类型。 但是,情况并非总是如此,有时您必须处理一个不捆绑其自己的类型模块声明的库。 在这种情况下,如果你想保持你的代码完全类型安全,你必须自己创建模块声明。

例如,假设您正在使用一个名为 example-vector3 的向量库,它使用一个方法 add 导出单个类 Vector3。 该方法用于将两个 Vector3 向量相加。

库中的代码可能类似于以下内容:

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
  }
}

这导出了一个类,该类创建具有 xyz 属性的向量,用于表示向量的坐标分量。

接下来,看一下使用假设库的示例代码:

索引.ts

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

OutputCannot find module 'example-vector3' or its corresponding type declarations. ts(2307)

为了解决这个问题,您现在将为这个包创建一个类型声明文件。 首先,创建一个名为 types/example-vector3/index.d.ts 的新文件并在您喜欢的编辑器中打开它。 在此文件中写入以下代码:

类型/example-vector3/index.d.ts

declare module "example-vector3" {
  export = vector3;

  namespace vector3 {
  }
}

在此代码中,您正在为 example-vector3 模块创建类型声明。 代码的第一部分是 declare module 块本身。 TypeScript 编译器将解析这个块并解释其中的所有内容,就好像它是模块本身的类型表示一样。 这意味着您在此处声明的任何内容,TypeScript 都将用于推断模块的类型。 现在,您说此模块导出一个名为 vector3 的命名空间,该命名空间当前为空。

保存并退出此文件。

TypeScript 编译器当前不知道您的声明文件,因此您必须将其包含在您的 tsconfig.json 中。 为此,通过将 types 属性添加到 compilerOptions 选项来编辑项目 tsconfig.json

tsconfig.json

{
  "compilerOptions": {
    ...
    "types": ["./types/example-vector3/index.d.ts"]
  }
}

现在,如果您返回原始代码,您将看到错误已更改。 TypeScript 编译器现在给出错误 2305

OutputModule '"example-vector3"' has no exported member 'Vector3'. ts(2305)

当您为 example-vector3 创建模块声明时,导出当前设置为空命名空间。 没有从该命名空间中导出的 Vector3 类。

重新打开types/example-vector3/index.d.ts,编写如下代码:

类型/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 的类,它在构造函数中接受三个数字,并且有一个 add 方法用于添加两个 [X180X ] 个实例一起,返回一个新实例作为结果。 您无需在此处提供实现,只需提供类型信息本身。 不提供实现的声明在 TypeScript 中称为 ambient 声明,通常在 .d.ts 文件中创建这些声明。

此代码现在将正确编译并具有 Vector3 类的正确类型。

使用命名空间,您可以将库导出的内容隔离到单个类型单元中,在本例中为 vector3 命名空间。 这使得自定义模块声明变得更加容易,甚至可以通过将类型声明提交到 DefinetelyTyped 存储库 来使所有开发人员都可以使用该类型声明。

结论

在本教程中,您了解了 TypeScript 中命名空间的基本语法,并检查了 TypeScript 编译器将其更改为的 JavaScript。 您还尝试了命名空间的一个常见用例:为尚未键入的外部库提供环境类型。

虽然不推荐使用命名空间,但并不总是建议在代码库中使用命名空间作为代码组织机制。 现代代码应该使用 ES 模块语法 ,因为它具有命名空间提供的所有功能,并且从 ECMAScript 2015 开始,它成为规范的一部分。 但是,在创建模块声明时,仍然建议使用命名空间,因为它允许更简洁的类型声明。

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