命名空间

关于术语的说明: 需要注意的是,在 TypeScript 1.5 中,术语发生了变化。 “内部模块”现在称为“命名空间”。 “外部模块”现在简称为“模块”,以与 ECMAScript 2015 的术语保持一致,(即 module X { 等同于现在更推荐的 namespace X {)。

本帖概述了在 TypeScript 中使用命名空间(以前称为“内部模块”)组织代码的各种方法。正如我们在关于术语的说明中所提到的,“内部模块”现在被称为“命名空间”。此外,在声明内部模块时,任何使用 module 关键字的地方,都可以且应该改用 namespace 关键字。这可以避免通过过多类似名称的术语来让新用户感到困惑。

🌐 This post outlines the various ways to organize your code using namespaces (previously “internal modules”) in TypeScript. As we alluded in our note about terminology, “internal modules” are now referred to as “namespaces”. Additionally, anywhere the module keyword was used when declaring an internal module, the namespace keyword can and should be used instead. This avoids confusing new users by overloading them with similarly named terms.

第一步

🌐 First steps

让我们从我们将在本页中作为示例使用的程序开始。我们编写了一小套简单的字符串验证器,就像你在网页表单中检查用户输入或检查外部提供的数据文件格式时可能编写的一样。

🌐 Let’s start with the program we’ll be using as our example throughout this page. We’ve written a small set of simplistic string validators, as you might write to check a user’s input on a form in a webpage or check the format of an externally-provided data file.

单个文件中的验证器

🌐 Validators in a single file

ts
interface StringValidator {
isAcceptable(s: string): boolean;
}
let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: StringValidator } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`'${s}' ${isMatch ? "matches" : "does not match"} '${name}'.`);
}
}

命名空间

🌐 Namespacing

随着我们增加更多的验证器,我们将希望有某种组织方案,这样我们就可以跟踪我们的类型,而不必担心与其他对象的名称冲突。与其将许多不同的名称放入全局命名空间,不如将我们的对象封装在一个命名空间中。

🌐 As we add more validators, we’re going to want to have some kind of organization scheme so that we can keep track of our types and not worry about name collisions with other objects. Instead of putting lots of different names into the global namespace, let’s wrap up our objects into a namespace.

在这个例子中,我们将所有与验证器相关的实体移动到一个名为 Validation 的命名空间中。因为我们希望这里的接口和类在命名空间外部可见,所以我们在它们前面加上了 export。相反,变量 lettersRegexpnumberRegexp 是实现细节,因此它们保持未导出状态,在命名空间外的代码不可见。在文件底部的测试代码中,我们现在需要在命名空间外使用类型时对其名称进行限定,例如 Validation.LettersOnlyValidator

🌐 In this example, we’ll move all validator-related entities into a namespace called Validation. Because we want the interfaces and classes here to be visible outside the namespace, we preface them with export. Conversely, the variables lettersRegexp and numberRegexp are implementation details, so they are left unexported and will not be visible to code outside the namespace. In the test code at the bottom of the file, we now need to qualify the names of the types when used outside the namespace, e.g. Validation.LettersOnlyValidator.

命名空间验证器

🌐 Namespaced Validators

ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${
validators[name].isAcceptable(s) ? "matches" : "does not match"
} ${name}`
);
}
}

跨文件拆分

🌐 Splitting Across Files

随着我们的应用的增长,我们希望将代码拆分到多个文件中以使其更易于维护。

🌐 As our application grows, we’ll want to split the code across multiple files to make it easier to maintain.

多文件命名空间

🌐 Multi-file namespaces

在这里,我们将把 Validation 命名空间拆分到多个文件中。虽然这些文件是分开的,但它们都可以贡献到同一个命名空间,并且可以像它们都在同一位置定义一样被使用。因为文件之间存在依赖,我们将添加引用标签来告诉编译器文件之间的关系。我们的测试代码除此之外保持不变。

🌐 Here, we’ll split our Validation namespace across many files. Even though the files are separate, they can each contribute to the same namespace and can be consumed as if they were all defined in one place. Because there are dependencies between files, we’ll add reference tags to tell the compiler about the relationships between the files. Our test code is otherwise unchanged.

Validation.ts
ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
LettersOnlyValidator.ts
ts
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
ZipCodeValidator.ts
ts
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
Test.ts
ts
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${
validators[name].isAcceptable(s) ? "matches" : "does not match"
} ${name}`
);
}
}

一旦涉及多个文件,我们需要确保所有编译后的代码都被加载。有两种方法可以做到这一点。

🌐 Once there are multiple files involved, we’ll need to make sure all of the compiled code gets loaded. There are two ways of doing this.

首先,我们可以使用 outFile 选项的连接输出,将所有输入文件编译成一个单独的 JavaScript 输出文件:

🌐 First, we can use concatenated output using the outFile option to compile all of the input files into a single JavaScript output file:

tsc --outFile sample.js Test.ts

编译器将根据文件中存在的引用标签自动排序输出文件。你也可以单独指定每个文件:

🌐 The compiler will automatically order the output file based on the reference tags present in the files. You can also specify each file individually:

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

或者,我们可以使用按文件编译(默认方式)为每个输入文件生成一个 JavaScript 文件。如果生成多个 JS 文件,我们需要在网页上使用 <script> 标签按适当顺序加载每个生成的文件,例如:

🌐 Alternatively, we can use per-file compilation (the default) to emit one JavaScript file for each input file. If multiple JS files get produced, we’ll need to use <script> tags on our webpage to load each emitted file in the appropriate order, for example:

MyTestPage.html(摘录)

🌐 MyTestPage.html (excerpt)

html
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

别名

🌐 Aliases

另一种简化命名空间操作的方法是使用 import q = x.y.z 为常用对象创建更短的名称。不要将其与用于加载模块的 import x = require("name") 语法混淆,这种语法只是为指定的符号创建一个别名。你可以将这种导入(通常称为别名)用于任何类型的标识符,包括从模块导入创建的对象。

🌐 Another way that you can simplify working with namespaces is to use import q = x.y.z to create shorter names for commonly-used objects. Not to be confused with the import x = require("name") syntax used to load modules, this syntax simply creates an alias for the specified symbol. You can use these sorts of imports (commonly referred to as aliases) for any kind of identifier, including objects created from module imports.

ts
namespace Shapes {
export namespace Polygons {
export class Triangle {}
export class Square {}
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as 'new Shapes.Polygons.Square()'

请注意,我们没有使用 require 关键字;相反,我们直接从要导入的符号的限定名称进行赋值。这类似于使用 var,但也适用于导入符号的类型和命名空间意义。重要的是,对于值而言,import 与原始符号是不同的引用,因此对别名 var 的更改不会反映在原始变量中。

🌐 Notice that we don’t use the require keyword; instead we assign directly from the qualified name of the symbol we’re importing. This is similar to using var, but also works on the type and namespace meanings of the imported symbol. Importantly, for values, import is a distinct reference from the original symbol, so changes to an aliased var will not be reflected in the original variable.

使用其他 JavaScript 库

🌐 Working with Other JavaScript Libraries

要描述不是用 TypeScript 编写的库的形状,我们需要声明库所暴露的 API。因为大多数 JavaScript 库只暴露少数几个顶层对象,命名空间是表示它们的好方法。

🌐 To describe the shape of libraries not written in TypeScript, we need to declare the API that the library exposes. Because most JavaScript libraries expose only a few top-level objects, namespaces are a good way to represent them.

我们将不定义实现的声明称为“外部声明”。 通常这些定义在 .d.ts 文件中。 如果你熟悉 C/C++,可以把它们看作是 .h 文件。 我们来看几个例子。

🌐 We call declarations that don’t define an implementation “ambient”. Typically these are defined in .d.ts files. If you’re familiar with C/C++, you can think of these as .h files. Let’s look at a few examples.

环境命名空间

🌐 Ambient Namespaces

流行的库 D3 在一个名为 d3 的全局对象中定义其功能。由于这个库是通过 <script> 标签(而不是模块加载器)加载的,它的声明使用命名空间来定义其结构。为了让 TypeScript 编译器能够看到这个结构,我们使用了一个全局命名空间声明。例如,我们可以这样开始编写它:

🌐 The popular library D3 defines its functionality in a global object called d3. Because this library is loaded through a <script> tag (instead of a module loader), its declaration uses namespaces to define its shape. For the TypeScript compiler to see this shape, we use an ambient namespace declaration. For example, we could begin writing it as follows:

D3.d.ts(简化摘录)

🌐 D3.d.ts (simplified excerpt)

ts
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}
export interface Event {
x: number;
y: number;
}
export interface Base extends Selectors {
event: Event;
}
}
declare var d3: D3.Base;