如何将命名空间与 TypeScript 外部模块一起使用?
Posted
技术标签:
【中文标题】如何将命名空间与 TypeScript 外部模块一起使用?【英文标题】:How do I use namespaces with TypeScript external modules? 【发布时间】:2015-08-02 04:05:41 【问题描述】:我有一些代码:
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
。跨文件组合同一命名空间中的多个对象是行不通的。我该怎么做?
【问题讨论】:
【参考方案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 ...
您已创建此设置:
每个模块(一张纸)都有自己的杯子,命名为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
:
此设置很有用,但不适用于模块(因为模块不会污染全局范围)。
版本 3:无罩杯
回到最初的例子,杯子A
、A
和A
对你没有任何好处。相反,您可以将代码编写为:
Mod1.ts
export class Twix ...
Mod2.ts
export class PeanutButterCup ...
Mod3.ts
export class KitKat ...
创建如下所示的图片:
好多了!
现在,如果您仍然在考虑是否真的想在模块中使用命名空间,请继续阅读...
这些不是您要寻找的概念
我们需要回到命名空间最初存在的根源,并检查这些原因是否对外部模块有意义。
组织:命名空间便于将逻辑相关的对象和类型组合在一起。例如,在 C# 中,您将在 System.Collections
中找到所有集合类型。通过将我们的类型组织到分层命名空间中,我们为这些类型的用户提供了良好的“发现”体验。
名称冲突:命名空间对于避免命名冲突很重要。例如,您可能有My.Application.Customer.AddForm
和My.Application.Order.AddForm
——这两种类型名称相同,但命名空间不同。在所有标识符都存在于同一根范围内并且所有程序集都加载所有类型的语言中,将所有内容都放在命名空间中至关重要。
这些原因在外部模块中有意义吗?
组织:外部模块必然已经存在于文件系统中。我们必须通过路径和文件名来解决它们,所以有一个逻辑组织方案供我们使用。我们可以有一个/collections/generic/
文件夹,里面有一个list
模块。
名称冲突:这根本不适用于外部模块。 在一个模块内,没有合理的理由让两个对象具有相同的名称。从消费端来看,任何给定模块的consumer 都可以选择他们将用来引用该模块的名称,因此不可能发生意外的命名冲突。
即使您不相信这些原因可以通过模块的工作方式充分解决,尝试在外部模块中使用命名空间的“解决方案”甚至不起作用。
盒中盒盒中盒
一个故事:
你的朋友鲍勃给你打电话。 “我家里有一个很棒的新组织计划”,他说,“来看看吧!”。没关系,我们去看看 Bob 想出了什么。
你从厨房开始,打开储藏室。有 60 个不同的盒子,每个盒子都标有“Pantry”。你随机选择一个盒子并打开它。里面是一个标有“谷物”的盒子。您打开“谷物”框,找到一个标有“意大利面”的框。打开“Pasta”框,找到一个标有“Penne”的框。你打开这个盒子,正如你所料,发现了一袋通心粉。
有点困惑,你拿起一个相邻的盒子,也标有“Pantry”。里面是一个盒子,同样标有“谷物”。打开“Grains”框,再次找到一个标有“Pasta”的框。你打开“Pasta”盒子,找到一个盒子,这个盒子标有“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();
如果你要导出大量的东西,那么你才应该使用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 class
或export function
而不是export default
的文件
在顶层具有相同export module Foo
的多个文件(不要认为这些文件会合并为一个Foo
!)
【讨论】:
这是一个非答案。您不应该或不希望外部模块命名空间的前提是错误的。虽然文件系统是一种您可以有点用于这些目的的组织方案,但对于消费者来说,使用来自给定项目的 n 个类或函数的 n 个 import 语句几乎没有那么好;特别是因为当您在实际代码中遇到问题时,它也会混淆命名约定。 无论人们多么想要它,它仍然是不可能。 我不明白,我们不再写帕斯卡了。从什么时候开始使用文件系统进行组织? 您可以通过一个“包装器”模块来导入和重新导出图书馆消费者感兴趣的所有内容。但同样,使用“命名空间”除了强制使用您的代码的任何人使用另一个级别的间接之外,不会提供任何价值。 写得很好,谢谢。我觉得你应该从 www.typescriptlang.org/docs/handbook/namespaces.html 链接到这个。我一定已经阅读了 typescriptlang.org 链接 3 或 4 次,作为 C# 开发人员,我自然希望将所有内容都放在命名空间中。我读过一些建议说不要,但没有解释为什么,也没有像这样确定(并且描述得很好)的。加上打字稿文档中没有提到这个 AFAIK【参考方案2】:尝试按文件夹整理:
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 暴露给消费者,这与您使用的模块系统类型无关该项目。
【讨论】:
LivingThings.dog.Dog 就是你所拥有的。 我建议保持字母大小写一致,如果导出“Tree”,则导入“Tree”,而不是“tree”。 另外,如果tree.ts
根本没有导出成员,你怎么能从它导入任何东西呢?
Man TS 肯定有一些愚蠢的旧语法,例如 import
和 require
在一个语句中。【参考方案3】:
Ryan 的回答没有错,但是对于那些来这里寻找如何维护 one-class-per-file 结构同时仍然正确使用 ES6 命名空间的人,请参阅 this 有用的资源来自微软。
阅读文档后我不清楚的一件事是:如何使用 single 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();
这种别名绝对是可恶的,所以不要这样做。你最好用上面的方法。就个人而言,我更喜欢“桶”。
【讨论】:
什么是“ES6 命名空间”? @AluanHaddad 在导入 es2015+ 时,导入的东西要么是默认的,要么是解构的,要么是命名空间的。const fs = require('fs')
、fs
是命名空间。 import * as moment from 'moment'
、moment
是命名空间。这是本体,而不是规范。
我知道这一点,但您最好在回答中解释一下。然而,ES6 命名空间实际上是一个东西,require
示例不适用于它们,原因有很多,包括可能无法调用 ES6 命名空间,而 require
返回一个很可能是可调用的普通对象。
我不关注,因为无论导入的东西是否可调用,它仍然可以作为一个命名空间从逻辑上讲。我认为这些警告对我上面的回答并不重要。【参考方案4】:
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);
【讨论】:
谢谢!只是想说对现有答案的更改最好不要作为新答案发布:它们应该作为对现有答案的评论添加,或者(更好)应该通过建议对您希望的答案进行编辑来建议改进。【参考方案5】: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
【讨论】:
【参考方案6】:OP,我和你在一起。 同样,300+ 票的答案没有错,但我的意见是:
将课程单独放入舒适温暖的自己的文件有什么问题? 我的意思是这会让事情看起来好多了,对吧? (或者像所有模型的 1000 行文件)
1234563为什么里面有这么多*?它应该简单、整洁,仅此而已。为什么我需要进口?为什么? C# 有命名空间是有原因的。到那时,您实际上是在使用“filenames.ts”作为标识符。作为标识符......现在来吧它的 2017 年,我们仍然这样做吗?我回到火星再睡一千年。
很遗憾,我的回答是:不,如果您不使用所有这些导入或使用这些文件名作为标识符(我认为这真的很愚蠢),那么您就无法使“命名空间”的东西发挥作用。另一种选择是:将所有这些依赖项放入一个名为 filenameasidentifier.ts 的框中并使用
export namespace(or module) boxInBox .
包装它们,这样当它们只是试图从位于它们之上的类中获取引用时,它们就不会尝试访问具有相同名称的其他类。
【讨论】:
【参考方案7】:我在这个主题周围看到的几个问题/cmets 听起来好像这个人正在使用Namespace
,它们的意思是“模块别名”。正如 Ryan Cavanaugh 在他的一个 cmets 中提到的那样,您可以让一个“包装器”模块重新导出多个模块。
如果您真的想从相同的模块名称/别名中全部导入,请将包装器模块与 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 ...
]
【讨论】:
【参考方案8】:试试这个命名空间模块
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');
【讨论】:
【参考方案9】:组织代码的正确方法是使用单独的目录代替命名空间。每个类都在它自己的文件中,在它各自的命名空间文件夹中。 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);
【讨论】:
这是误导,绝对不正确。这不是命名空间的工作方式。它也没有回答操作问题。【参考方案10】:您可以使用* as wrapper_var
语法使wrapper_var
下的所有导入方法都可以访问:
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
【讨论】:
以上是关于如何将命名空间与 TypeScript 外部模块一起使用?的主要内容,如果未能解决你的问题,请参考以下文章