TypeScript 的秘密平行宇宙
大约四年前,我还是个 TypeScript 新用户,对这种新学习的 JavaScript 方言为我带来的可能性感到惊叹。但就像所有 TypeScript 开发者一样,我很快就遇到了一些难以调试的问题。
在 TypeScript 领域,这些问题通常源于程序员对语言本身缺乏了解。
我想向你介绍我早期遇到的一个问题,主要是因为它与 TypeScript 教程中最鲜为人知的主题之一有关:类型作用域。一旦你意识到它的存在,它就显而易见了,但对我来说,当我不知道它的时候,它经常让我感到困惑。
问题
我的问题实际上非常简单:我正在构建一个库,其中包含分布在各个文件夹中的一堆类。
对于库的公共 API,我希望将这些类公开为单个嵌套对象(例如,该类Console
可用作Output/Console.ts
)API.Output.Console
。
于是我定义了一些命名空间,导入了这些类——然后就遇到了麻烦。我就是无法从命名空间内部重新导出这些类。🤨
第一次尝试,结果发现 TypeScript 无效:
import Console from './Output/Console'
export namespace Output {
export Console
// TS Error 1128: Declaration or statement expected.
}
也许我需要用其他名称导入它。好的,第二次尝试:
import ConsoleAlias from './Output/Console'
export namespace Output {
export Console = ConsoleAlias
// TS Error 2304: Cannot find name 'Console'.
}
第三次尝试——或许用 ES 模块的方式可以解决问题。(剧透:没用。)
import ConsoleAlias from './Output/Console'
export namespace Output {
export { ConsoleAlias as Console }
//TS Error 1194: Export declarations are not permitted in a namespace.
}
第四次尝试使用export const
。这确实编译了。🎉
import ConsoleAlias from './Output/Console'
export namespace Output {
export const Console = ConsoleAlias
}
但不幸的是,每当我想用该对象输入提示时API
,我都会收到以下错误:
import * as API from './API'
let console: API.Output.Console
// TS Error 2694: Namespace 'API.Output' has no exported member 'Console'.
……但我导出了那个成员!为什么它不见了?
嗯。也许这export const
就是问题所在。我应该用一些神奇的 TypeScript 关键字来解决这个问题,然后再试试export type
。
废话不多说:第五次尝试,编译通过!🥳
import ConsoleAlias from './Output/Console'
export namespace Output {
export type Console = ConsoleAlias
}
好吧,现实检查一下:类型提示有用吗?确实有用!🕺
……但这种喜悦也只是昙花一现。当我尝试创建一个新Console
实例时,TypeScript 错误又一次出现了:
import * as API from './API'
const console = new API.Output.Console()
// TS Error 2708: Cannot use namespace 'API' as a value.
哦,来吧。
不用说,那时我已经对 TypeScript 感到厌烦了。
然而,我不想相信我的问题没有解决方案,所以我去了StackOverflow,几天后没有得到答复,我直接在 TypeScript 的 GitHub 存储库中创建了一个问题。
解决方案
TypeScript 团队的Ryan非常友好,几分钟内就回答了我的问题。对我来说,解决方案似乎非常明显,但同时也非常晦涩难懂:
我采用了这种方法——并且非常有效。
它为何有效
当时,我只是接受了这个答案,并在代码中使用了它。这听起来似乎很有道理——const
和type
都能以某种方式工作,所以我只需要把它们结合起来就能让两个用例都正常工作。但我心里还是有点不安。为什么我可以以相同的名称导出两个东西而不会产生一个巨大的编译器错误?
我花了几个月(甚至几年)的 TypeScript 经验才完全理解为什么这样做,但我认为这种见解对其他人也可能有价值,所以我将在这里分享:
TypeScript 有一个秘密范围。
TypeScript 维护着一个完全独立于 JavaScript 变量作用域的类型作用域。这意味着你可以在同一个文件中foo
声明一个变量和一个类型。🤯 它们甚至不需要兼容:foo
const foo = 'bar'
type foo = number
// ✅ This is absolutely fine for TypeScript
TypeScript 中的类有点特殊。如果你定义一个类Foo
,TypeScript 不仅会创建一个变量Foo
(包含该类对象本身),还会声明一个类型Foo
,表示该类的一个实例Foo
。
class Foo {}
// We can use Foo as a type
let foo: Foo
// We can use Foo as a constructor (i.e. a value)
const bar = new Foo()
类似地,当从另一个文件导入名称时(就像我们ConsoleAlias
在第二个代码示例中所做的那样),ConsoleAlias
类对象和ConsoleAlias
类型都会被导入。
换句话说,该单一名称(导入的ConsoleAlias
)同时包含类对象和在中声明的类型Output/Console.ts
。
Console
因此,如果我们在命名空间内部Output
通过写入来重新导出,则只会导出export const Console = ConsoleAlias
类对象const
(因为 a只保存值,而不保存类型)。同样,如果我们执行,则只会导出export type Console = ConsoleAlias
类类型。
简而言之:由于作用域独立,使用相同名称导出值和类型都是有效的。在某些情况下(例如上面提到的情况),这不仅有效,而且是必要的。
我希望这有助于完善你对 TypeScript 的心理模型。🤓
文章来源:https://dev.to/loilo/typescript-s-secret-parallel-universe-54i6