TypeScript Importing a type with the same name as a namespace import, ideally alongside it

bvjveswy  于 4个月前  发布在  TypeScript
关注(0)|答案(5)|浏览(62)

建议

🔍 搜索词

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-tsEffect 之类的库中遇到了这个问题。
一个模块还可以导出它所需的类型,如输入类型,因此这个更雄心勃勃的要求不仅对于提及“构造函数”类型有用,尽管这种用例的重要性较小。
一种解决方法是导出一个别名(例如 Foo.OfFoo.Self ):

export { Foo as Self }

如果我们谈论一个库,这是维护者需要支持的事情,而且并不是那么显而易见。
另一个选择是在导入文件中创建一个类型别名:

import * as Foo from './Foo.js';
type Foo = Foo.Foo;

如果我们的类型需要泛型和类型约束,这将成为要求维护的真正麻烦事。
为了与后者的解决方法保持一致,支持更保守的要求是一致的。目前它会产生错误 Duplicate identifier 'Foo' ,这有点奇怪,因为值和类型生活在不同的世界里。

yx2lnoni

yx2lnoni1#

允许这种问题存在会导致在 ./Foo.js 发生更改时产生关键的向前兼容性问题。例如,假设 Foo.js

export interface Foo { }
export const x = 5;

到目前为止一切正常。然后 Foo.js 对模块进行“无害”的升级:

export class Foo { }
export const x = 5;

现在你导入了一个名为 Foo 的值(模块对象),以及另一个名为 Foo 的值(类构造函数)。
“等等”,你说,“这是一个类型导入,这里没有冲突”。但这并不是 type { Foo } 所做的——它所做的是创建一个具有导入对象所有含义的 仅类型导入。例如,你可以编写:

import type { Bar } from "./x.js";
type A = typeof Bar;

其中 typeof 运算符正在查询名为 Bar 的值的类型。

import * as Foo from './Foo.js';
type Foo = Foo.Foo;

允许这种结构的原因是因为我们有一个错误,它没有得到适当的标记,并发现太多的代码库依赖于这个错误。我认为我们不希望在这个基础上进一步打开谷仓的门。

vql8enpb

vql8enpb2#

@RyanCavanaugh

允许这种行为的问题在于,当 ./Foo.js 发生变化时,它会创建一个关键的前向兼容性问题。但这是 TypeScript 在处理导入类型和命名空间之间的冲突时的当前行为。例如,当我们有一个 foo.ts,它只导出一个类型或接口:

// @filename: Foo.ts
export interface Foo { id: number }

从另一个具有相同名称的命名空间中导入类型 Foo 是合法的,该命名空间具有一些值导出。

// @filename: app.ts
import type { Foo } from "./Foo";
namespace Foo {
    export const x = { id: 1 };
}
const x: Foo = Foo.x; // <- this is valid

Foo.ts 的导出从类型更改为类时,编译器将开始抱怨冲突,这似乎是合理的。

// @filename: Foo2.ts
export class Foo { id!: number }

(Workbench)

请注意,这种行为在 4.5.5 和 4.6.4 之间发生了变化。在 4.5.5 之前的版本中两者都会报告错误,而在新版本中只有后者会,因此我认为这是一个预期的行为。由于 import * as Foo 无非是一个具有一些值导出的本地 namespace Foo,为了保持一致性,最好将其行为保持与上述相同。也就是说,以下代码:

import * as Foo from './Foo';
import type { Foo } from './Foo';

Foo.ts 导出的符号 Foo 仅具有类型意义(类型或接口)时应编译,但在具有值意义(类)时应报告错误。

s2j5cfk0

s2j5cfk03#

我理解你不想让它成为一个事物的其他原因,但是关于向前兼容的脚炮:

export interface Foo<T> { _tag: 'Foo', _value: T };

export const of = <A>(a: A) => new FooImpl(a) as Foo<A>;

class FooImpl<T> implements Foo<T> {
    _tag = 'Foo' as const;
    _value!: T;
    constructor(private value: T) {}
}

你不能将 new Foo 传递给一个函数。在FP的上下文中,创建一个工厂函数更加方便。在模块内部使用类是完全可能的。标签可以用于保留值的类型信息并使类型名义化。

am46iovg

am46iovg4#

即使需要一些仪式,所以它是明确的选择。桶装进口正变得越来越流行,并被TypeScript手册推荐而不是命名空间。有很多理由来赋予使用这种模式的开发人员权力

fumotvh3

fumotvh35#

这将非常有用。我经常想知道为什么TypeScript中的类型不自动定义自己的命名空间 - 这是次优的,至少允许开发者回收命名简单性,从而实现。

相关问题