如何在TypeScript外部模块中使用命名空间?

292

I have some code:

baseTypes.ts

export namespace Living.Things {
  export class Animal {
    move() { /* ... */ }
  }
  export class Plant {
    photosynthesize() { /* ... */ }
  }
}

dog.ts

import b = require('./baseTypes');

export namespace Living.Things {
  // Error, can't find name 'Animal', ??
  export class Dog extends Animal {
    woof() { }
  }
}

tree.ts

// Error, can't use the same name twice, ??
import b = require('./baseTypes');
import b = require('./dogs');

namespace Living.Things {
  // Why do I have to write b.Living.Things.Plant instead of b.Plant??
  class Tree extends b.Living.Things.Plant {

  }
}

这真的很令人困惑。我希望有一堆外部模块都可以在同一个命名空间 Living.Things 中贡献类型。看起来这根本行不通 -- 我在 dogs.ts 中看不到 Animal。我必须在 tree.ts 中写完整的命名空间名称 b.Living.Things.Plant。无法将多个对象合并到同一文件中的相同命名空间。我该怎么做呢?

10个回答

1152

糖果杯比喻

版本1:每个糖果都有一个杯子

假设你写了以下代码:

Mod1.ts

export namespace A {
    export class Twix { ... }
}

Mod2.ts

export namespace A {
    export class PeanutButterCup { ... }
}

Mod3.ts

export namespace A {
     export class KitKat { ... }
}

你创建了这个设置:enter image description here 每个模块(纸张)都有自己的杯子,名称为A。这是没有意义的 - 你并没有真正地在这里组织你的糖果,你只是在你和这些糖果之间增加了一个额外的步骤(从杯子里拿出来)。

版本2:全局范围内的一个杯子

如果你不使用模块,你可能会像这样编写代码(请注意缺少export声明): global1.ts
namespace A {
    export class Twix { ... }
}

global2.ts

namespace A {
    export class PeanutButterCup { ... }
}

global3.ts

namespace A {
     export class KitKat { ... }
}

这段代码在全局作用域内创建了一个合并的命名空间A

enter image description here

虽然这个设置很有用,但它在模块的情况下不适用(因为模块不会污染全局作用域)。


第三版:无杯

回到原始示例,杯子 AAA 对你没有任何好处。相反,您可以将代码编写为:

Mod1.ts

export class Twix { ... }

Mod2.ts

export class PeanutButterCup { ... }

Mod3.ts

export class KitKat { ... }

创建一个类似这样的图片:

enter image description here

好多了!

现在,如果你还在考虑是否真的想要在模块中使用命名空间,请继续阅读...


这些不是你要找的概念

我们需要回到为什么首先存在命名空间的根源,并检查这些原因是否对外部模块有意义。

组织:命名空间非常适用于将逻辑相关的对象和类型分组。例如,在 C# 中,你会在 System.Collections 中找到所有的集合类型。通过将我们的类型组织成分层级别的命名空间,我们为那些类型的用户提供了良好的“发现”体验。

名称冲突:命名空间很重要,以避免命名冲突。例如,你可能有My.Application.Customer.AddFormMy.Application.Order.AddForm - 两个同名但命名空间不同的类型。在一种语言中,所有标识符都存在于同一个根作用域中,并且所有程序集都加载所有类型,因此将所有内容都放在命名空间中至关重要。

这些原因在外部模块中是否有意义?

组织:外部模块已经存在于文件系统中。我们必须通过路径和文件名解析它们,因此有一个逻辑组织方案可供我们使用。我们可以在其中包含一个/collections/generic/文件夹和一个list模块。

名称冲突:这在外部模块中根本不适用。在模块内部,没有两个对象具有相同的名称的情况。从消费者的角度来看,消费者任何给定模块都可以选择用于引用模块的名称,因此不可能发生意外的命名冲突。


即使您认为模块如何工作并没有充分解决那些原因,试图在外部模块中使用命名空间的“解决方案”甚至都行不通。

盒中的盒中的盒

一个故事:

Your friend Bob has come up with a new organization scheme for his house, but it is not very useful. He has labeled 60 boxes in the pantry, each with the same label "Pantry". Inside each box, there is another box labeled "Grains", and inside that box, there is another box labeled "Pasta". Finally, inside the "Pasta" box, you can find the specific type of pasta, such as penne or rigatoni. However, this system requires opening many boxes to access any particular item, making it less convenient than simply storing everything on a shelf. When you question Bob about his system, he explains that he created a namespace for all his pasta to prevent others from putting non-pasta items in the "Pantry" namespace. However, his system is unnecessarily complicated and confusing.
In programming, external modules are like their own boxes. Each module contains its own functionality and should be organized accordingly. Instead of using namespaces, it is better to organize modules based on their functionality.
When organizing modules, it is best to export them as close to the top-level as possible. If you are only exporting a single class or function, use "export default". For example: MyClass.ts
export default class SomeType {
  constructor() { ... }
}

MyFunc.ts

function getThing() { return 'thing'; }
export default getThing;

消费

import t from './MyClass';
import f from './MyFunc';
var x = new t();
console.log(f());

这对消费者来说是最优的。他们可以随意命名您的类型(在此示例中为t),而无需进行任何额外的点操作来找到您的对象。

  • 如果要导出多个对象,请将它们全部放在顶层:

MyThings.ts

export class SomeType { ... }
export function someFunc() { ... }

消耗

import * as m from './MyThings';
var x = new m.SomeType();
var y = m.someFunc();
  • 如果您要导出大量内容,那么才应该使用module/namespace关键字:

MyLargeModule.ts

export namespace Animals {
  export class Dog { ... }
  export class Cat { ... }
}
export namespace Plants {
  export class Tree { ... }
}

消费

import { Animals, Plants} from './MyLargeModule';
var x = new Animals.Dog();

红旗标志

以下所有内容都是模块结构的红旗标志。如果您的文件符合以下任何一项,请仔细检查,确保您没有尝试将外部模块命名空间化:

  • 一个文件的唯一顶级声明是 export module Foo { ... }(删除 Foo 并将所有内容“上移”一级)
  • 一个文件只有一个 export classexport function,而不是 export default
  • 多个文件在顶层具有相同的 export module Foo {(不要认为这些会合并为一个 Foo!)

115
这是一个非答案。认为外部模块不需要或不应该使用名称空间的前提是错误的。虽然文件系统可以_有点_用作这些目的的组织方案,但这并不像消费者从给定项目中使用n个类或函数所需的n个导入语句那样好;特别是当你在实际代码中时,它也会混淆命名约定。 - Albinofrenchy
16
无论一个人多么希望,它仍然是不可能的 - Ryan Cavanaugh
32
我不明白,我们已经不再写Pascal了。自从什么时候用文件系统来组织东西成为了一种可行的方式? - David
13
您可以通过编写一个“包装器”模块来导入并重新导出您的库中对用户有用的所有内容。但是,再次强调,使用“命名空间”除了为使用您的代码的任何人强制增加另一层间接性之外,不会提供任何价值。 - Ryan Cavanaugh
显示剩余17条评论

66

对于那些寻找如何在使用ES6名称空间的同时仍保持每个文件一个类结构的人,请参考Microsoft提供的有用资源。Ryan的回答没有问题,但有一件事情我在阅读文档后不清楚:如何使用单个import导入整个(合并的)模块。

编辑 回过头来更新这个答案。TS中出现了几种命名空间的方法。

所有模块类都在一个文件中。

export namespace Shapes {
    export class Triangle {}
    export class Square {}      
}

将文件导入命名空间,并重新分配

import { Triangle as _Triangle } from './triangle';
import { Square as _Square } from './square';

export namespace Shapes {
  export const Triangle = _Triangle;
  export const Square = _Square;
}

// ./shapes/index.ts
export { Triangle } from './triangle';
export { Square } from './square';

// in importing file:
import * as Shapes from './shapes/index.ts';
// by node module convention, you can ignore '/index.ts':
import * as Shapes from './shapes';
let myTriangle = new Shapes.Triangle();

最后一个考虑因素。您可以为每个文件设置命名空间

// triangle.ts
export namespace Shapes {
    export class Triangle {}
}

// square.ts
export namespace Shapes {
    export class Square {}
}

但是当从同一个命名空间导入两个类时,TS会抱怨有重复的标识符。目前唯一的解决方案是给该命名空间创建一个别名。

import { Shapes } from './square';
import { Shapes as _Shapes } from './triangle';

// ugh
let myTriangle = new _Shapes.Shapes.Triangle();

这种别名现象非常让人厌恶,所以不要这么做。你最好采用上面的方法。就我个人而言,我更喜欢“桶形畸变”。


9
「ES6命名空间」是指在ECMAScript 6标准中引入的一种特性,用于将代码块封装在独立的命名空间中,以避免与其他模块或全局作用域中的变量名发生冲突。 - Aluan Haddad
1
@AluanHaddad,当导入es2015+时,导入的内容可以是默认值、解构值或命名空间。例如:const fs = require('fs'),其中fs就是命名空间;而import * as moment from 'moment'中的moment也是一个命名空间。这是本体论,而非规范。 - Jefftopia
1
我知道这一点,但你最好在回答中解释一下。ES6命名空间实际上是存在的,然而require示例并不适用于它们,原因有很多,包括ES6命名空间可能无法被调用,而require返回一个普通对象,该对象可能是可调用的。 - Aluan Haddad
1
我不太明白,因为从逻辑上讲,无论导入的东西是否可调用,它仍然作为一个命名空间。我认为这些注意事项对我上面的答案并不重要。 - Jefftopia

7

请尝试按文件夹组织:

baseTypes.ts

(基础类型)
export class Animal {
    move() { /* ... */ }
}

export class Plant {
    photosynthesize() { /* ... */ }
}

dog.ts

import b = require('./baseTypes');

export class Dog extends b.Animal {
    woof() { }
}   

tree.ts

import b = require('./baseTypes');

class Tree extends b.Plant {
}

LivingThings.ts

import dog = require('./dog')
import tree = require('./tree')

export = {
    dog: dog,
    tree: tree
}

main.ts

import LivingThings = require('./LivingThings');
console.log(LivingThings.Tree)
console.log(LivingThings.Dog)

这个想法是你的模块本身不应该关心或知道它们正在参与某个命名空间,但是这样以一种紧凑而合理的方式向消费者公开API,而且不会受到项目中使用哪种类型的模块系统的影响。


9
生物.狗.这里有一只狗。 - Corey Alix
我建议保持大小写的一致性,如果你导出的是 "Tree",那么导入时应该使用 "Tree" 而不是 "tree"。 - demisx
1
另外,当tree.ts没有任何导出成员时,你如何从中导入任何内容? - demisx
Man TS 确实有一些古怪的语法,比如在一个语句中同时使用 importrequire - Andy

5

请尝试使用这个命名空间模块

namespaceModuleFile.ts

export namespace Bookname{
export class Snows{
    name:any;
    constructor(bookname){
        console.log(bookname);
    }
}
export class Adventure{
    name:any;
    constructor(bookname){
        console.log(bookname);
    }
}
}





export namespace TreeList{
export class MangoTree{
    name:any;
    constructor(treeName){
        console.log(treeName);
    }
}
export class GuvavaTree{
    name:any;
    constructor(treeName){
        console.log(treeName);
    }
}
}

bookTreeCombine.ts

---编译部分---

import {Bookname , TreeList} from './namespaceModule';
import b = require('./namespaceModule');
let BooknameLists = new Bookname.Adventure('Pirate treasure');
BooknameLists = new Bookname.Snows('ways to write a book'); 
const TreeLis = new TreeList.MangoTree('trees present in nature');
const TreeLists = new TreeList.GuvavaTree('trees are the celebraties');

4

我看到关于这个主题的一些问题/评论,感觉这些人使用Namespace时实际上他们是想要使用“模块别名”。如Ryan Cavanaugh在他的评论中提到的,你可以有一个“Wrapper”模块重新导出多个模块。

如果你真的想要从相同的模块名称/别名导入所有内容,请结合一个路径映射在你的tsconfig.json中使用包装器模块。

示例:

./path/to/CompanyName.Products/Foo.ts

export class Foo {
    ...
}


./path/to/CompanyName.Products/Bar.ts

export class Bar {
    ...
}


./path/to/CompanyName.Products/index.ts

export { Foo } from './Foo';
export { Bar } from './Bar';



tsconfig.json

{
    "compilerOptions": {
        ...
        paths: {
            ...
            "CompanyName.Products": ["./path/to/CompanyName.Products/index"],
            ...
        }
        ...
    }
    ...
}



main.ts

import { Foo, Bar } from 'CompanyName.Products'

注意:输出的.js文件中的模块解析需要进行处理,可以使用https://github.com/tleunen/babel-plugin-module-resolver等工具。

以下是处理别名解析的.babelrc示例:

{
    "plugins": [
        [ "module-resolver", {
            "cwd": "babelrc",
            "alias": {
                "CompanyName.Products": "./path/to/typescript/build/output/CompanyName.Products/index.js"
            }
        }],
        ... other plugins ...
    ]
}

3

OP,我支持你。 虽然那个得到了300多个赞的答案没有错,但是我的观点是:

  1. 把类放在它们自己温暖舒适的文件中有什么问题吗? 我的意思是这样做会让事情看起来更好,对吧?(或者有人只喜欢一个包含所有模型的1000行文件)

  2. 所以,如果第一个目标实现了,我们就必须要导入导入导入……在每个模型文件中都要导入,像个男人一样,一个模型文件,一个.d.ts文件,为什么里面有这么多星号?应该简单、整洁,就这样。为什么我需要在那里导入?为什么?C#有命名空间是有原因的。

  3. 而且到那时,你实际上是把“filenames.ts”作为标识符。作为标识符……来吧,现在已经是2017年了,我们还在这样做吗?我要回到火星再睡1000年。

所以不幸的是,我的答案是:不行,如果你不使用所有这些导入或使用这些文件名作为标识符(我认为这真的很愚蠢),你不能使“命名空间”这个东西起作用。另一个选择是:把所有的依赖放到一个叫做filenameasidentifier.ts的盒子里,并使用

export namespace(or module) boxInBox {} .

将它们包装起来,这样当它们只是想从直接位于其上方的类中获取引用时,它们就不会尝试访问其他具有相同名称的类。


3

Albinofrenchy的答案稍有改进:

base.ts

export class Animal {
move() { /* ... */ }
}

export class Plant {
  photosynthesize() { /* ... */ }
}

dog.ts

import * as b from './base';

export class Dog extends b.Animal {
   woof() { }
} 

things.ts

import { Dog } from './dog'

namespace things {
  export const dog = Dog;
}

export = things;

main.ts

import * as things from './things';

console.log(things.dog);

2
谢谢!只是想说,对于现有答案的更改最好不要发布为新答案:它们应该作为对现有答案的评论添加,或者(更好的选择)应该通过建议编辑您希望改进的答案来建议。 - a3nm

1
您可以使用* as wrapper_var语法,使得所有导入的方法都可以在wrapper_var下访问:
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

0

dog.ts

import b = require('./baseTypes');

export module Living.Things {
    // Error, can't find name 'Animal', ??
    // Solved: can find, if properly referenced; exporting modules is useless, anyhow
    export class Dog extends b.Living.Things.Animal {
        public woof(): void {
            return;
        }
    }
}

tree.ts

// Error, can't use the same name twice, ??
// Solved: cannot declare let or const variable twice in same scope either: just use a different name
import b = require('./baseTypes');
import d = require('./dog');

module Living.Things {
    // Why do I have to write b.Living.Things.Plant instead of b.Plant??
    class Tree extends b.Living.Things.Plant {
    }
}

-7
正确组织代码的方法是使用单独的目录代替命名空间。每个类将在其相应的命名空间文件夹中的自己的文件中。index.ts仅重新导出每个文件;index.ts文件中不应有实际代码。像这样组织您的代码使得导航更加容易,并且基于目录结构是自我记录的。
// index.ts
import * as greeter from './greeter';
import * as somethingElse from './somethingElse';

export {greeter, somethingElse};

// greeter/index.ts
export * from './greetings.js';
...

// greeter/greetings.ts
export const helloWorld = "Hello World";

你可以像这样使用它:
import { greeter } from 'your-package'; //Import it like normal, be it from an NPM module or from a directory.
// You can also use the following syntax, if you prefer:
import * as package from 'your-package';

console.log(greeter.helloWorld);

3
这是误导性的,完全是错误的。这不是命名空间的工作方式。此外,它没有回答运营问题。 - AndrewMcLagan
我同意Andrew的观点。要理解TypeScript如何使用“模块”和“命名空间”,最好参考文档。 请检查您的TypeScript版本,因为这可能会影响“命名空间”和“模块”的使用。我通过遵循文档以及这个stack post这个和最后一个这个成功让我的工作。希望它有所帮助 =)。祝编码愉快! - 0xe1λ7r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接