这篇文章概述了在 TypeScript 中使用模块和命名空间组织代码的各种方式。我们还将讨论一些关于如何使用命名空间和模块的高级话题,并解决在 TypeScript 中使用它们时的一些常见问题。
🌐 This post outlines the various ways to organize your code using modules and namespaces in TypeScript. We’ll also go over some advanced topics of how to use namespaces and modules, and address some common pitfalls when using them in TypeScript.
有关 ES 模块的更多信息,请参阅 模块 文档。 有关 TypeScript 命名空间的更多信息,请参阅 命名空间 文档。
🌐 See the Modules documentation for more information about ES Modules. See the Namespaces documentation for more information about TypeScript namespaces.
注意:在非常旧的 TypeScript 版本中,命名空间被称为“内部模块”,它们早于 JavaScript 模块系统。
🌐 Note: In very old versions of TypeScript namespaces were called ‘Internal Modules’, these pre-date JavaScript module systems.
使用模块
🌐 Using Modules
模块可以包含代码和声明。
🌐 Modules can contain both code and declarations.
模块还依赖于模块加载器(如 CommonJs/Require.js)或支持 ES 模块的运行时。模块能够提供更好的代码复用、更强的隔离性以及更好的打包工具支持。
🌐 Modules also have a dependency on a module loader (such as CommonJs/Require.js) or a runtime which supports ES Modules. Modules provide for better code reuse, stronger isolation and better tooling support for bundling.
同样值得注意的是,对于 Node.js 应用,模块是默认的,并且我们在现代代码中推荐使用模块而不是命名空间。
🌐 It is also worth noting that, for Node.js applications, modules are the default and we recommended modules over namespaces in modern code.
从 ECMAScript 2015 开始,模块成为语言的原生部分,并且应该被所有兼容的引擎实现所支持。因此,对于新项目,推荐使用模块作为代码组织的机制。
🌐 Starting with ECMAScript 2015, modules are native part of the language, and should be supported by all compliant engine implementations. Thus, for new projects modules would be the recommended code organization mechanism.
使用命名空间
🌐 Using Namespaces
命名空间是 TypeScript 特有的一种组织代码的方式。
命名空间只是全局命名空间中的命名 JavaScript 对象。
这使得命名空间成为一种非常简单易用的结构。
与模块不同,它们可以跨多个文件,并且可以使用 outFile 进行连接。
在 Web 应用中,命名空间可以是组织代码的好方法,所有依赖都可以作为 <script> 标签包含在 HTML 页面中。
🌐 Namespaces are a TypeScript-specific way to organize code.
Namespaces are simply named JavaScript objects in the global namespace.
This makes namespaces a very simple construct to use.
Unlike modules, they can span multiple files, and can be concatenated using outFile.
Namespaces can be a good way to structure your code in a Web Application, with all dependencies included as <script> tags in your HTML page.
就像所有全局命名空间污染一样,很难识别组件依赖,尤其是在大型应用中。
🌐 Just like all global namespace pollution, it can be hard to identify component dependencies, especially in a large application.
命名空间和模块的陷阱
🌐 Pitfalls of Namespaces and Modules
在本节中,我们将描述使用命名空间和模块的各种常见陷阱,以及如何避免它们。
🌐 In this section we’ll describe various common pitfalls in using namespaces and modules, and how to avoid them.
/// <reference> 模块化
🌐 /// <reference>-ing a module
一个常见的错误是尝试使用 /// <reference ... /> 语法来引用模块文件,而不是使用 import 语句。要理解两者的区别,我们首先需要了解编译器如何根据 import 的路径(例如 import x from "...";、import x = require("..."); 等中的 ...)来定位模块的类型信息。
🌐 A common mistake is to try to use the /// <reference ... /> syntax to refer to a module file, rather than using an import statement.
To understand the distinction, we first need to understand how the compiler can locate the type information for a module based on the path of an import (e.g. the ... in import x from "...";, import x = require("...");, etc.) path.
编译器将尝试查找具有适当路径的 .ts、.tsx,然后是 .d.ts。如果找不到特定文件,编译器将查找 ambient module 声明。请记住,这些需要在 .d.ts 文件中声明。
🌐 The compiler will try to find a .ts, .tsx, and then a .d.ts with the appropriate path.
If a specific file could not be found, then the compiler will look for an ambient module declaration.
Recall that these need to be declared in a .d.ts file.
-
myModules.d.tsts// In a .d.ts file or .ts file that is not a module:declare module "SomeModule" {export function fn(): string;} -
myOtherModule.tsts/// <reference path="myModules.d.ts" />import * as m from "SomeModule";
这里的引用标签允许我们定位包含全局模块声明的声明文件。这就是几个 TypeScript 示例中使用的 node.d.ts 文件被使用的方式。
🌐 The reference tag here allows us to locate the declaration file that contains the declaration for the ambient module.
This is how the node.d.ts file that several of the TypeScript samples use is consumed.
不必要的命名空间
🌐 Needless Namespacing
如果你要将程序从命名空间转换为模块,很容易得到如下所示的文件:
🌐 If you’re converting a program from namespaces to modules, it can be easy to end up with a file that looks like this:
-
shapes.tstsexport namespace Shapes {export class Triangle {/* ... */}export class Square {/* ... */}}
这里的顶层命名空间 Shapes 无缘无故地封装了 Triangle 和 Square。这对你的模块使用者来说令人困惑且烦人:
🌐 The top-level namespace here Shapes wraps up Triangle and Square for no reason.
This is confusing and annoying for consumers of your module:
-
shapeConsumer.tstsimport * as shapes from "./shapes";let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
TypeScript 模块的一个关键特性是两个不同的模块永远不会向同一个作用域贡献名称。因为模块的使用者可以决定给它分配什么名称,所以无需主动将导出的符号封装在命名空间中。
🌐 A key feature of modules in TypeScript is that two different modules will never contribute names to the same scope. Because the consumer of a module decides what name to assign it, there’s no need to proactively wrap up the exported symbols in a namespace.
重申一下为什么你不应该尝试给模块内容使用命名空间,命名空间的一般概念是为了提供构造的逻辑分组以及防止名称冲突。因为模块文件本身已经是一个逻辑分组,其顶层名称由导入它的代码定义,所以没有必要为导出的对象使用额外的模块层。
🌐 To reiterate why you shouldn’t try to namespace your module contents, the general idea of namespacing is to provide logical grouping of constructs and to prevent name collisions. Because the module file itself is already a logical grouping, and its top-level name is defined by the code that imports it, it’s unnecessary to use an additional module layer for exported objects.
这是一个修改后的例子:
🌐 Here’s a revised example:
-
shapes.tstsexport class Triangle {/* ... */}export class Square {/* ... */} -
shapeConsumer.tstsimport * as shapes from "./shapes";let t = new shapes.Triangle();
模块的权衡
🌐 Trade-offs of Modules
正如 JS 文件与模块之间存在一一对应关系一样,TypeScript 的模块源文件与它们生成的 JS 文件之间也存在一一对应关系。 这样做的一个结果是,根据你所针对的模块系统,不可能将多个模块源文件进行连接。 例如,在针对 commonjs 或 umd 时,你不能使用 outFile 选项,但从 TypeScript 1.8 及更高版本起,可以 在针对 amd 或 system 时使用 outFile。
🌐 Just as there is a one-to-one correspondence between JS files and modules, TypeScript has a one-to-one correspondence between module source files and their emitted JS files.
One effect of this is that it’s not possible to concatenate multiple module source files depending on the module system you target.
For instance, you can’t use the outFile option while targeting commonjs or umd, but with TypeScript 1.8 and later, it’s possible to use outFile when targeting amd or system.