如何将命名空间与 TypeScript 外部模块一起使用?糖果杯类比这些不是您要寻找的概念外部模块指南红旗

2022-08-30 00:15:25

我有一些代码:

baseTypes.ts

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

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.ThingsAnimaldogs.tsb.Living.Things.Planttree.ts


答案 1

糖果杯类比

版本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

全局1.ts

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

全局2.ts

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

全局3.ts

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

代码在全局范围内创建一个合并的命名空间:A

enter image description here

此设置很有用,但不适用于模块的情况(因为模块不会污染全局范围)。


版本3:无杯

回到原来的例子, 杯子 , ,并没有帮你任何忙。相反,您可以将代码编写为: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

名称冲突:这在外部模块中根本不适用。模块中,没有合理的理由拥有两个同名的对象。从消费方面来看,任何给定模块的消费者都可以选择他们将用于引用模块的名称,因此意外的命名冲突是不可能的。


即使您不相信模块的工作方式没有充分解决这些原因,尝试在外部模块中使用命名空间的“解决方案”甚至不起作用。

箱中箱子盒

一个故事:

你的朋友鲍勃打电话给你。“我家里有一个很棒的新组织计划,”他说,“来看看吧!整洁,让我们去看看鲍勃想出了什么。

你从厨房开始,打开储藏室。有60个不同的盒子,每个盒子都标有“食品储藏室”。你随机选择一个盒子并打开它。里面是一个标有“谷物”的盒子。你打开“谷物”盒子,找到一个标有“意大利面”的盒子。您打开“意大利面”框,找到一个标有“Penne”的框。你打开这个盒子,发现,正如你所料,一袋便士意大利面。

有点困惑,你拿起一个相邻的盒子,也标有“食品储藏室”。里面是一个盒子,再次标有“谷物”。你打开“谷物”盒子,再次找到一个标有“意大利面”的盒子。你打开“意大利面”盒子,找到一个盒子,这个盒子被标记为“Rigatoni”。你打开这个盒子,发现...一袋里加托尼意大利面。

“太好了!”鲍勃说。“一切都在命名空间中!

“可是鲍勃...”你回复。“你的组织计划是无用的。你必须打开一堆盒子才能得到任何东西,实际上找到任何东西并不比你把所有东西都放在一个盒子里而不是个盒子里更方便。事实上,由于您的食品储藏室已经逐个货架分类,因此您根本不需要这些盒子。为什么不把意大利面放在架子上,在你需要的时候把它捡起来呢?”

“你不明白 - 我需要确保没有其他人把不属于'Pantry'命名空间的东西放进去。我已经安全地将所有意大利面组织到命名空间中,因此我可以轻松找到它”Pantry.Grains.Pasta

鲍勃是一个非常困惑的人。

模块是他们自己的盒子

你可能在现实生活中发生过类似的事情:你在亚马逊上订购了一些东西,每件商品都出现在自己的盒子里,里面有一个较小的盒子,你的商品包装在自己的包装里。即使内箱相似,货物也没有有用的“组合”。

与盒子类比,关键观察是外部模块是它们自己的盒子。它可能是一个非常复杂的项目,具有许多功能,但任何给定的外部模块都是它自己的盒子。


外部模块指南

现在我们已经知道我们不需要使用“命名空间”,我们应该如何组织我们的模块?以下是一些指导原则和示例。

导出尽可能接近顶级

  • 如果只导出单个类或函数,请使用:export default

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();
  • 如果要导出大量内容,则只有这样才应使用 / 关键字:modulenamespace

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 functionexport default
  • 在顶级具有相同内容的多个文件(不要以为这些文件会合并为一个!export module Foo {Foo

答案 2

Ryan的回答没有错,但是对于那些来到这里寻找如何在正确使用ES6命名空间的同时维护每个文件一个类的结构的人来说,请参阅Microsoft的这个有用的资源。

阅读文档后,我不清楚的一件事是:如何使用单个导入整个(合并)模块。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();

这种混叠绝对是令人憎恶的,所以不要这样做。你最好采用上述方法。就个人而言,我更喜欢“桶”。