建议
🔍 搜索词
import, type, module, namespace import,
✅ 可实现性检查清单
我的建议满足以下准则:
- 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
- 这不会改变现有JavaScript代码的运行时行为
- 这可以在不根据表达式的类型发出不同的JS的情况下实现
- 这不是一个运行时特性(例如库功能、具有JavaScript输出的非ECMAScript语法、新的JS语法糖等)
- 这个特性将与TypeScript's Design Goals的其他部分保持一致。
⭐ 建议
能够编写类似这样的代码:
import * as Foo, type { Foo } from './Foo.js';
一个更保守的要求可能是:
import * as Foo from './Foo.js';
import type { Foo } from './Foo.js';
📃 动机示例
import * as Foo, type { Foo } from './Foo.js';
// no need to write Foo.Foo vvv
const someFunction = (someFoo: Foo) => ...
💻 用例
在函数式编程中,用模块替换类是一种常见的模式。这有几个优点(可摇树性、无 this
绑定等)。
这些模块捆绑了操作某些数据和标识此数据的类型的函数。
export type Foo = [string, (a: any) => unknown];
export const unit = (): Foo => ['', (x: any) => x];
export const concat = (self: Foo, a: Foo) =>
[self[0] + a[0], (x: any) => a[1](self[1](x))];
问题在于,当你需要在作为命名空间导入时提及数据的类型时,你发现自己重复了两次名称:
import * as Foo from './Foo.js';
const someFunction = (someFoo: Foo.Foo) => ...
除了看起来有点奇怪之外,当名称变长时,这实际上并不方便,但你又不想直接导入成员,因为像 concat
这样的功能名很可能会导致冲突,而将它别名为 concatFoo
又是一个繁琐的过程,可能会使文件的导入部分变得非常嘈杂且难以阅读。
我在自己的代码中遇到了这个问题,也在诸如 fp-ts 和 Effect 之类的库中遇到了这个问题。
一个模块还可以导出它所需的类型,如输入类型,因此这个更雄心勃勃的要求不仅对于提及“构造函数”类型有用,尽管这种用例的重要性较小。
一种解决方法是导出一个别名(例如 Foo.Of
或 Foo.Self
):
export { Foo as Self }
如果我们谈论一个库,这是维护者需要支持的事情,而且并不是那么显而易见。
另一个选择是在导入文件中创建一个类型别名:
import * as Foo from './Foo.js';
type Foo = Foo.Foo;
如果我们的类型需要泛型和类型约束,这将成为要求维护的真正麻烦事。
为了与后者的解决方法保持一致,支持更保守的要求是一致的。目前它会产生错误 Duplicate identifier 'Foo'
,这有点奇怪,因为值和类型生活在不同的世界里。
5条答案
按热度按时间yx2lnoni1#
允许这种问题存在会导致在
./Foo.js
发生更改时产生关键的向前兼容性问题。例如,假设Foo.js
有到目前为止一切正常。然后
Foo.js
对模块进行“无害”的升级:现在你导入了一个名为
Foo
的值(模块对象),以及另一个名为Foo
的值(类构造函数)。“等等”,你说,“这是一个类型导入,这里没有冲突”。但这并不是
type { Foo }
所做的——它所做的是创建一个具有导入对象所有含义的 仅类型导入。例如,你可以编写:其中
typeof
运算符正在查询名为Bar
的值的类型。允许这种结构的原因是因为我们有一个错误,它没有得到适当的标记,并发现太多的代码库依赖于这个错误。我认为我们不希望在这个基础上进一步打开谷仓的门。
vql8enpb2#
@RyanCavanaugh
允许这种行为的问题在于,当 ./Foo.js 发生变化时,它会创建一个关键的前向兼容性问题。但这是 TypeScript 在处理导入类型和命名空间之间的冲突时的当前行为。例如,当我们有一个
foo.ts
,它只导出一个类型或接口:从另一个具有相同名称的命名空间中导入类型
Foo
是合法的,该命名空间具有一些值导出。当
Foo.ts
的导出从类型更改为类时,编译器将开始抱怨冲突,这似乎是合理的。(Workbench)
请注意,这种行为在 4.5.5 和 4.6.4 之间发生了变化。在 4.5.5 之前的版本中两者都会报告错误,而在新版本中只有后者会,因此我认为这是一个预期的行为。由于
import * as Foo
无非是一个具有一些值导出的本地namespace Foo
,为了保持一致性,最好将其行为保持与上述相同。也就是说,以下代码:在
Foo.ts
导出的符号Foo
仅具有类型意义(类型或接口)时应编译,但在具有值意义(类)时应报告错误。s2j5cfk03#
我理解你不想让它成为一个事物的其他原因,但是关于向前兼容的脚炮:
你不能将
new Foo
传递给一个函数。在FP的上下文中,创建一个工厂函数更加方便。在模块内部使用类是完全可能的。标签可以用于保留值的类型信息并使类型名义化。am46iovg4#
即使需要一些仪式,所以它是明确的选择。桶装进口正变得越来越流行,并被TypeScript手册推荐而不是命名空间。有很多理由来赋予使用这种模式的开发人员权力
fumotvh35#
这将非常有用。我经常想知道为什么TypeScript中的类型不自动定义自己的命名空间 - 这是次优的,至少允许开发者回收命名简单性,从而实现。