====== Article ======
Official Website: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
From: TypeScript 怎么声明全局类型,可以不 import 直接使用? - Snowflyt的回答 - 知乎 https://www.zhihu.com/question/350961609/answer/3559626500
比较奇怪五年过去了这个问题下边的回答还是讲得不太明白。
要搞明白这个问题,首先要大致懂一些 ''TS'' 的模块机制,要不然会云里雾里,不知道为什么有些回答提的方案可以工作。
目前我们通常使用的都是现代化的模块机制,要么是 ''ESM'' 要么是 ''CommonJS'',它们的特点是每个文件都是一个模块,都是相对独立的。你要在一个模块中用另一个模块的东西,肯定要导入进来才能用——实际上大多数现代编程语言也是这么做的,像是 ''Java'' 也是用 ''package'' 做隔离的,只是做了一些特殊处理,位于 ''java.lang'' 包下面的东西可以在全局直接使用,不需要手动导入,这也是为啥你可以在 ''Java'' 里直接用 ''String'' 而不需要导入它。
所以我们有第一种思路——你说像 ''Java'' 这样的其他编程语言提供了特殊机制能搞一个“全局模块”(虽然 ''Java'' 里这个实际上叫 ''package'',''module'' 是另一个东西),那么 ''TS'' 有没有提供这种东西呢?
有!但是首先,让我们看一下 ''TS'' 提供的一个叫做声明合并(''Declaration Merging'')的功能,你可以修改一个已经存在的 ''interface'',给它添加属性:
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
const box: Box = { height: 5, width: 6, scale: 10 };
你可以看到,''TS'' 允许你重复定义同一个 ''interface'',最终 ''TS'' 会把这些声明合并到一块。
这东西光看本身好像没什么用——但是如果你想修改其他模块里的 ''interface'',你也可以使用声明合并功能添加属性。比如说 ''Vue 2'' 常用的组件库 ''Element UI'',它会往 this 上挂一些 ''this.$notify'' 或 ''this.$message'' 之类的“全局方法”来方便使用,那么它是怎么给这些方法做 ''TS'' 支持的呢?
declare module "vue/types/vue" {
interface Vue {
/** Used to show feedback after an activity. The difference with Notification is that the latter is often used to show a system level passive notification. */
$message: ElMessage
}
}
现在你看到了这种语法——在普通的 ''interface'' 的声明合并外头套一层 ''declare module ... { ... }'',表示你需要修改的是一个特定模块的导出,比如这里就给 ''Vue'' 实例加了一个 ''$message'' 方法。这个在文档里叫做 ''Module Augmentation''.
TS 文档中没提的是,''Module Augmentation'' 不止支持你给 ''interface'' 做声明合并,也支持创建新类型。比如说你想给 ''express'' 搞个新类型,以便之后直接从 ''express'' 导入:
// 这个文件随便放在什么地方都行,TS 会自动识别
declare module "express" {
type MyUser = {
username: string;
password: string;
};
}
// 在另一个文件里
import type { MyUser } from "express";
聪明的你大概想到了,''TS'' 应该也提供了一个“全局模块”,允许你这么干。它的语法稍微有些特别:
// 某个文件里
declare global {
type MyType = string;
}
export {};
// 另一个文件里
type A = MyType; // 可以直接用了
注意,这里的 ''export {}'' 是为了确保 ''TS'' 将该文件视为模块——实际上,只要该文件中出现了任何 ''import'' 或 ''export'' 语句,''TS'' 就会将一个文件视为模块,这里是为了以防万一。你现在可能对此感到困惑,在下面的“第二种方案”里头我会详细描述这里的原理。
和 ''Module Augmentation'' 类似,这个叫做 ''Global Augmentation'',语法几乎是一样的,只是从 ''declare module "..."'' 改成了 ''declare global''.
那么这个 ''Augmentation'' 该放在哪个文件里呢?答案是随便放在哪个文件里,只要被 ''tsconfig.json'' 的 ''"include"'' 属性包含了就行。聪明的 ''TS'' 会自动检索文件目录,把它应用到全局。
不过 ''TS'' 也不是总那么聪明——假如你故意把这个 ''Augmentation'' 放到 ''node_modules'' 之类的地方,TS 当然不会帮你检索出来。所以有时为了以防万一,你可以手动导入一下该模块——比如,假设你把所有这种 ''Augmentation'' 放到了一个 ''global-augmentation.ts'' 文件中:
import "../types/global-augmentation";
type A = MyType;
这可能看上去还是有点烦,但已经比之前好很多了——现在你只需要为保险起见在每个文件开头写一行这样的导入,而不用对每个常用的类型做单独导入。当然,实际上你在绝大多数情况下不需要写这一行,这只是为了保险。
不过,即使为了保险起见,你还是觉得每个文件顶上加一行导入很麻烦。有没有更好的办法?也有,那就是修改 ''tsconfig.json'',你可以把该文件所在目录放到 ''typeRoots'' 里头,这样能确保 ''TS'' 会去检索该目录中的类型定义:
{
"compilerOptions": {
"typeRoots": ["./types"]
}
}
有时安装一些库时也会提示你在 ''typeRoots'' 中加一些东西,这基本说明该库要么在全局增加了一些类型定义,要么是修改了另一个库的类型定义——比如 ''Element UI'' 这样的库,你需要修改 ''typeRoots'' 使 ''TS'' 识别出 ''Vue Instance'' 上现在有了 ''this.$notify'' 和 ''this.$message'' 这样的属性,否则 ''TS'' 遇到这些属性会报错。
上面介绍的第一种方法(也就是 ''declare global'')是我比较推荐的方案。还有另一种常说的方法即 ''declare namespace'' 的做法实际上利用了某种 ''Hacking'',就是我将要的说的第二种方法。我不推荐使用这种做法,但也可以解释一下原理。
我在开头提到,现在我们常用的 ''ESM'' 和 ''CommonJS'' 都是 ''module'' 方案——但是在更早的时候呢?很长一段时间以来,很多跑在浏览器中的 ''JS'' 代码都不用任何真正的模块方案,你项目里的各个 ''JS'' 文件虽然貌似是分开来的,但它们其实在同一个全局作用域里工作,通过 '''' 导入,大概会发现你好像可以直接在代码里用 ''_.xxx'' 而不需要 ''import'' 什么东西,就是这个原理。我估摸着在如今很多非前后端分离的(比如用后端模板引擎生成的)''Web'' 项目中还是会有很多这样的用法。
扯了这么多废话是为了引出一个问题——那么 ''TS'' 支不支持这东西呢?当然是支持的。这个叫做 ''Ambient Modules''——我们也知道,由于诸多的历史遗留问题,''JS'' 至少有五种甚至四种相互竞争的模块方案,直到今天还有 ''ESM'' 和 ''CJS'' 纠缠不休。那么 ''TS'' 作为一个自 2012 年出现的编程语言,中间这些年肯定为各种模块方案做过支持,详细可以看 ''TS'' 文档中的这块 Reference.
如果你不愿意完整读完这个 Reference,可以看看我的简单解释。
首先不知道大家是否思考过一个问题:''TS'' 怎么知道某个文件是个 ''module''?毕竟浏览器同时提供了旧式的 ''