作者选择了 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-typescript、 types和 interfaces. 在本教程中,您将创建和使用名称空间来说明语法及其用途。
前提条件
要遵循本教程,您将需要:
- 联合国 您可以执行 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
,y
和z
的矢量,旨在代表矢量的坐标组件。
接下来,看看使用假设库的示例代码:
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 系列页面。