如何在 TypeScript 中使用命名空间

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

介绍

TypeScript是JavaScript(https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript)语言的扩展,它使用JavaScript的运行时间与编译时间类型检查器。在TypeScript中,你可以使用 _namespaces_来组织你的代码。以前被称为内部模块,TypeScript的名空间是基于ECMAScript模块的早期草案。在ECMAScript规格草案中,内部模块在2013年9月(https://speakerdeck.com/dherman/september-2013-modules-status-update?slide=7)左右被删除,但TypeScript在不同的名称下保留了这个想法。

名称空间允许开发人员创建可以用来持有多个值的独立组织单位,如属性、 https://andsky.com/tech/tutorials/how-to-use-classes-in-typescripttypesinterfaces. 在本教程中,您将创建和使用名称空间来说明语法及其用途。

前提条件

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

  • 联合国 您可以执行 TypeScript 程序的环境与示例一起执行 。 要在您的本地机上设置此功能, 您需要安装如下:
  • 既 [Node] (https://nodejs.org/en/about/) (又 [npm] (https://docs.npmjs.com/about-npm (或 [yarn] (https://yarnpkg.com/getting-started )) , 以便运行一个处理 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游戏场中尝试这些好处. (英语)

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

在 TypeScript 中创建名称空间

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

例如,您将创建一个DatabaseEntity名空间来存储数据库实体,就好像您正在使用一个 Object–relational mapping (ORM) 库。

1namespace DatabaseEntity {
2}

这声明了DatabaseEntity名称空间,但尚未将代码添加到该名称空间中,然后在名称空间中添加一个用户类来代表数据库中的用户实体:

1namespace DatabaseEntity {
2  class User {
3    constructor(public name: string) {}
4  }
5}

您可以通常在名称空间中使用用户类,以说明此情况,创建一个新的用户实例,并将其存储在新用户变量中:

1namespace DatabaseEntity {
2  class User {
3    constructor(public name: string) {}
4  }
5
6  const newUser = new User("Jon");
7}

但是,如果您试图在名称空间之外使用用户,TypeScript 编译器会给您错误2339:

1[secondary_label Output]
2Property 'User' does not exist on type 'typeof DatabaseEntity'. (2339)

如果您想在名称空间外使用类别,则必须先导出用户类别以便外部可用,如下所示的突出代码所示:

1namespace DatabaseEntity {
2  export class User {
3    constructor(public name: string) {}
4  }
5
6  const newUser = new User("Jon");
7}

您现在可以使用其完全合格的名称访问数据库实体名称空间之外的用户类别,在这种情况下,完全合格的名称是`数据库实体。

1namespace DatabaseEntity {
2  export class User {
3    constructor(public name: string) {}
4  }
5
6  const newUser = new User("Jon");
7}
8
9const newUserOutsideNamespace = new DatabaseEntity.User("Jane");

您可以从名称空间中导出任何内容,包括变量,然后成为名称空间中的属性. 在下面的代码中,您正在导出新用户变量:

1namespace DatabaseEntity {
2  export class User {
3    constructor(public name: string) {}
4  }
5
6  export const newUser = new User("Jon");
7}
8
9console.log(DatabaseEntity.newUser.name);

由于newUser变量被导出,您可以将其作为名称空间的属性访问。

1[secondary_label Output]
2Jon

接口一样,TypeScript 中的命名空间也允许声明合并,这意味着同一个命名空间的多个声明将合并为一个单一声明。

使用上面的示例,这意味着如果您再次声明您的DatabaseEntity名称空间,您将能够用更多的属性扩展名称空间。

 1namespace DatabaseEntity {
 2  export class User {
 3    constructor(public name: string) {}
 4  }
 5
 6  export const newUser = new User("Jon");
 7}
 8
 9namespace DatabaseEntity {
10  export class UserRole {
11    constructor(public user: User, public role: string) {}
12  }
13
14  export const newUserRole = new UserRole(newUser, "admin");
15}

在你的新的DatabaseEntity名称空间声明中,你可以使用任何以前导出到DatabaseEntity名称空间的成员,包括从以前的声明中导出,而无需使用他们的完全合格名称。你正在使用该名称,因为它在第一个名称空间中被宣称为用户参数的类型,在UserRole构建器中设置为用户,并在创建一个新的UserRole实例时使用新用户值。

现在你已经看到了名称空间的基本语法,你可以继续研究名称空间是如何被TypeScript编译器翻译成JavaScript的。

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

TypeScript 中的命名空间不仅仅是一个编译时间功能,它们还会改变所产生的 JavaScript 代码. 要了解更多关于命名空间如何工作,您可以分析支持此 TypeScript 功能的 JavaScript。

拿出你在第一个示例中使用的代码:

1namespace DatabaseEntity {
2  export class User {
3    constructor(public name: string) {}
4  }
5
6  export const newUser = new User("Jon");
7}
8
9console.log(DatabaseEntity.newUser.name);

TypeScript 编译器将为此 TypeScript 片段生成以下 JavaScript 代码:

 1"use strict";
 2var DatabaseEntity;
 3(function (DatabaseEntity) {
 4    class User {
 5        constructor(name) {
 6            this.name = name;
 7        }
 8    }
 9    DatabaseEntity.User = User;
10    DatabaseEntity.newUser = new User("Jon");
11})(DatabaseEntity || (DatabaseEntity = {}));
12console.log(DatabaseEntity.newUser.name);

为了声明DatabaseEntity名称空间,TypeScript 编译器会创建一个名为DatabaseEntity的非初始化变量,然后创建一个 Immediately Invoked Function Expression](IIFE)。 这个 IIFE 会收到一个单一的参数,即DatabaseEntity Átha (DatabaseEntity = {}),这是DatabaseEntity变量的当前值。

将您的数据库实体的值设置为空值,当您将其传输到 IIFE 时,因为分配操作的返回值是被分配的值。

在IIFE中,创建用户类,然后分配给数据库实体对象的用户属性。

现在来看看第二个代码示例,在那里你有多个名称空间声明:

 1namespace DatabaseEntity {
 2  export class User {
 3    constructor(public name: string) {}
 4  }
 5
 6  export const newUser = new User("Jon");
 7}
 8
 9namespace DatabaseEntity {
10  export class UserRole {
11    constructor(public user: User, public role: string) {}
12  }
13
14  export const newUserRole = new UserRole(newUser, "admin");
15}

生成的JavaScript代码将看起来像这样:

 1"use strict";
 2var DatabaseEntity;
 3(function (DatabaseEntity) {
 4    class User {
 5        constructor(name) {
 6            this.name = name;
 7        }
 8    }
 9    DatabaseEntity.User = User;
10    DatabaseEntity.newUser = new User("Jon");
11})(DatabaseEntity || (DatabaseEntity = {}));
12(function (DatabaseEntity) {
13    class UserRole {
14        constructor(user, role) {
15            this.user = user;
16            this.role = role;
17        }
18    }
19    DatabaseEntity.UserRole = UserRole;
20    DatabaseEntity.newUserRole = new UserRole(DatabaseEntity.newUser, "admin");
21})(DatabaseEntity || (DatabaseEntity = {}));

代码的开始看起来和以前一样,没有初始化的变量DatabaseEntity,然后是一个IIFE,实际代码设置了DatabaseEntity对象的属性。

现在,当第二个IIFE执行时,‘DatabaseEntity’已经绑定到一个对象,所以你只是通过添加额外的属性来扩展到已经可用的对象。

现在你已经看到了TypeScript命名空间的语法以及它们如何在底层JavaScript中工作. 有了这个背景,你现在可以运行一个常见的命名空间用例:为外部库定义类型而无需键入。

使用名称空间为外部库提供打字

在本节中,您将通过名称空间有用的场景之一进行运行:为外部库创建模块声明. 要做到这一点,您将在您的TypeScript项目中写一个新文件来声明打字,然后更改您的tsconfig.json文件以使TypeScript 编译器识别该类型。

<$>[注] 注: 若要遵循以下步骤,需要使用具有访问文件系统的 TypeScript 环境。如果您正在使用 TypeScript Playground,您可以将现有的代码导出到 CodeSandbox 项目,点击顶部菜单中的 导出,然后点击 在 CodeSandbox 中打开

不是每个在 npm注册表中可用的包都有自己的TypeScript模块声明,这意味着在您项目中安装一个包时,您可能会遇到与该包缺少的类型声明相关的编译错误,或者您可能不得不使用具有所有类型设置为任何的库。

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

举个例子,想象一下你正在使用一个名为example-vector3的矢量库,该库导出一个单一的类,名为Vector3,使用一个单一的方法,名为add

图书馆中的代码可以看起来如下:

 1export class Vector3 {
 2  super(x, y, z) {
 3    this.x = x;
 4    this.y = y;
 5    this.z = z;
 6  }
 7
 8  add(vec) {
 9    let x = this.x + vector.x;
10    let y = this.y + vector.y;
11    let z = this.z + vector.z;
12
13    let newVector = new Vector3(x, y, z);
14
15    return newVector
16  }
17}

这输出了一个类,创建具有属性x,yz的矢量,旨在代表矢量的坐标组件。

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

1[label index.ts]
2import { Vector3 } from "example-vector3";
3
4const v1 = new Vector3(1, 2, 3);
5const v2 = new Vector3(1, 2, 3);
6
7const v3 = v1.add(v2);

example-vector3 库没有附带其自己的类型声明,因此 TypeScript 编译器将发出错误2307:

1[secondary_label Output]
2Cannot find module 'example-vector3' or its corresponding type declarations. ts(2307)

要解决此问题,您现在将为此包创建一个类型声明文件. 首先,创建一个名为 `types/example-vector3/index.d.ts’的新文件,并在您最喜欢的编辑器中打开它。

1[label types/example-vector3/index.d.ts]
2declare module "example-vector3" {
3  export = vector3;
4
5  namespace vector3 {
6  }
7}

在这个代码中,你正在创建模块的类型声明。代码的第一部分是模块本身的类型声明。TypeScript 编译器将对这个块进行分析,并将其内部的一切解释为模块本身的类型表示。这意味着你在这里声明的任何东西,TypeScript 都会用来推断模块的类型。 现在,你说这个模块出口了一个名称空间,名为vector3,目前是空的。

保存和退出此文件。

TypeScript 编译器目前不知道您的声明文件,所以您必须将其纳入您的 tsconfig.json. 要做到这一点,请通过在 compilerOptions 选项中添加 types 属性来编辑项目 tsconfig.json:

1[label tsconfig.json]
2{
3  "compilerOptions": {
4    ...
5    "types": ["./types/example-vector3/index.d.ts"]
6  }
7}

现在,如果你回到原始代码,你会看到错误发生了变化. TypeScript 编译器现在会给出错误 2305:

1[secondary_label Output]
2Module '"example-vector3"' has no exported member 'Vector3'. ts(2305)

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

重新打开types/example-vector3/index.d.ts并输入以下代码:

 1[label types/example-vector3/index.d.ts]
 2declare module "example-vector3" {
 3  export = vector3;
 4
 5  namespace vector3 {
 6    export class Vector3 {
 7      constructor(x: number, y: number, z: number);
 8      add(vec: Vector3): Vector3;
 9    }
10  }
11}

在此代码中,请注意您现在如何在vector3名称空间中导出一个类别。模块声明的主要目标是提供由库暴露的值的类型信息。

在这种情况下,你知道‘example-vector3’库提供了一个名为‘Vector3’的类,它在构建器中接受三个数字,并且它有一个‘add’ 方法,用于将两个‘Vector3’实例添加在一起,从而返回一个新的实例。

此代码现在将正确编译,并为Vector3类进行正确的类型。

使用名称空间,您可以将库导出的东西分离成一个单一的类型单元,在这种情况下是vector3名称空间,这使得更容易地定制模块声明,甚至通过将其提交到DefinetelyTyped存储库(https://github.com/DefinitelyTyped/DefinitelyTyped)为所有开发人员提供类型声明。

结论

在本教程中,您通过了TypeScript中的名空间的基本语法,并检查了TypeScript编译器将其变成的JavaScript,您还尝试了名空间的常见用例:为尚未打字的外部库提供环境打字。

虽然命名空间不被削减,但使用命名空间作为您的代码库中的代码组织机制并不总是建议的. 现代代码应该使用 ES 模块语法,因为它具有命名空间提供的所有功能,从 ECMAScript 2015 开始,它成为规范的一部分。

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

Published At
Categories with 技术
comments powered by Disqus