沐光

记录在前端之路的点点滴滴

由 shims-vue.d.ts 引发的思考

前言

由于项目近期进行 ts 迁移,作为第一个吃螃蟹的人,踩过了不少坑。迁移过程中遇到的大大小小的问题基本上都解决了,但是对于 shims-vue.d.ts 文件的命名以及其内的模块声明始终找不到比较贴切的解释。沉下心来读了些外网资料,总算是有点“豁开云雾见青天”的感觉了。此处就记录我对于 ts 全局模块声明的一些思考以及一些 ts 项目迁移遇到的坑。

Vue ts 声明文件

在安装 @vue/typescript 之后,项目会生成两个新文件,分别是 shims-vue.d.tsshims-jsx.d.ts,其内容分别是:

1
2
3
4
5
6
// shims-vue.d.ts

declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue, { VNode } from 'vue';

declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

那么这两个文档有什么作用呢?

shims-vue.d.ts

前者为 Ambient Declarations(通称:外部模块定义) ,主要为项目内所有的 vue 文件做模块声明,毕竟 ts 默认只识别 .d.ts、.ts、.tsx 后缀的文件;(即使补充了 Vue 得模块声明,IDE 还是没法识别 .vue 结尾的文件,这就是为什么引入 vue 文件时必须添加后缀的原因,不添加编译也不会报错)

shims-jsx.d.ts

后者为 JSX 语法的全局命名空间,这是因为基于值的元素会简单的在它所在的作用域里按标识符查找(此处使用的是无状态函数组件 (SFC)的方法来定义),当在 tsconfig 内开启了 jsx 语法支持后,其会自动识别对应的 .tsx 结尾的文件,可参考官网 jsx

产生的问题

首先,官方文档的上并没有将 shims-xxx.d.ts 做为通用的模板,其仅仅给我们列举了以下模板样例:

  • global-modifying-module.d.ts
  • global-plugin.d.ts
  • global.d.ts
  • module-class.d.ts
  • module-function.d.ts
  • module-plugin.d.ts
  • module.d.ts

那么该如何理解这两个文件?

是否能够更改在统一规范的文件内?

全局接口、命名空间、模块等声明又有那些写法来定义?该如何写?

… 对于产生的这么些问题,下面依次分析。

解惑

理解并改造 shims-xxx.d.ts

我们知道,xxx.d.ts 的文件表明,其内部的一些声明都为全局的声明,能够在项目各组件内都能获取到。因此 Vue 生成的两个 shims-xxx.d.ts 其实是为了表明,该两文件为 Vue 相关的全局声明文件。

但是从项目管理来说,随着引入的 npm 模块增多(比如公司内部 npm 源上的不带 types 的包),那么模仿 Vue 的声明文件写法,外部声明的文件也会越来越多,文件夹看起来就不是很舒服了。因此有没有一种比较好的方法来解决文件过多的问题呢?

对于我来说,我更偏向将这些简单的声明维护在一个 .d.ts 文件内,正好官网也推荐维护在一个大的 module 内,因此我们可以维护一个 module.d.ts 来总体声明所有的外部模块。基于官方的例子,我做了两个文件来管理外部模块的声明,分别是 module.d.tsdeclarations.d.ts。前者主要维护需要写的比较详细的外部模块,后者主要维护简写模式的模块(包括内部需要声明的 .js 文件,兼容历史遗留问题)。例如:

改造后的 module/index.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// This `declare module` is called ambient module, which is used to describe modules written in JavaScript.

// 添加 vue-clipboard2 的 Vue 插件声明
declare module 'vue-clipboard2' {
import { PluginFunction } from 'vue';
const clipboard: PluginFunction<any>;
// 定义默认导出的类型
export default clipboard;
}

// 添加 fe-monitor-sdk 的 Vue 插件声明
declare module 'fe-monitor-sdk' {
import { PluginObject } from 'vue';
// 定义解构的变量类型
export const monitorVue: PluginObject<any>;
}

// 添加所有 .vue 文件的声明
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

改造后的 module/declarations.d.ts

1
2
3
// Shorthand ambient modules, All imports from this shorthand module will have the any type.

declare module '@/cookie-set';

附加:对于 global 声明可视情况分类,比如通用的放在 global.d.ts,其余可视情况(如果该类型比较多的话)按照对应类型分类,比如 table 的可全部放在 global-table.d.ts

全局声明的写法

另一个一直比较疑惑的问题是全局声明的写法,比如模块的“单文件单模块声明”的写法“单文件多模块合并声明”的写法不太一样,“无导入的全局声明文件”和“带导入声明的全局声明文件”的写法又有些不同,这里我一一列出其可行的写法以及其不同的原因。

注:这里的一些定义都是个人总结的便于记忆的说法,为非标准定义。

单文件单模块声明

该文件支持两种写法,分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 写法一
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

// 写法二
import Vue from 'vue';

declare module '*.vue' {
export default Vue;
}

注: 前者(写法一)主要为无 ts 声明的模块添加声明,后者(写法二)主要为已有 types 声明的模块进行声明扩展(可以参考 vue-router 源码部分

单文件多模块合并声明

仅有一种写法(需要关闭对应的多次引入重复模块的 lint 规则或者忽略此 types 文件夹内的所有内容)

1
2
3
4
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
无导入的全局声明文件

无导入即没有 import 声明,直接定义全局接口、函数等

1
2
3
4
5
interface TableRenderParam extends BasicObject {
row: BasicObject;
key: string;
index?: number;
}
带导入声明的全局声明文件

带有 import 导入插件声明的必须显示定义 global,例如:

1
2
3
4
5
6
7
8
9
10
11
import { CreateElement } from 'vue';

// function 部分
declare global {
interface TableRenderFunc {
(h: CreateElement, { row, key, index }: TableRenderParam): JSX.Element;
}
}

// namespace 部分
declare global {}
不同的原因

如果在“单文件多模块合并声明”将 import 提出至最顶层时,会发现 ts 报错,说模块无法进一步扩大,为什么将 import 提出后会报错提示模块无法扩大?

个人研究得出的结论是,当将 import 提出至模块外时,就已经表明该文件内的其它 declare 的模块已经是存在 ts 声明的模块,此时再对其进行 declare 声明即对其原本的声明上进行扩展(可参考 vue-router 对于 vue 的扩展),但是对于没有 ts 声明的模块,我们拿不到它的 ts 声明,因此也就没发进行模块扩展,所以就会报错。

而将 import 放至模块内时,因为 module 本来就表明自己为一个模块,其就可以作为模块的声明,为没有对应声明的模块添加声明了。

此外,对于多个 declare global 的写法,此是采用了声明合并>)的方式,使得所有的模块声明都合并至同一个 global 全局声明中,因此,在对于将 import 提至外层的“带导入声明的全局声明文件”来说,分文件全局维护或者单文件声明合并式维护都是可行的。

TypeScript 与 ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。相反地,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

ts 踩坑记录

已经维护至博文项目 ts 迁移的踩坑记录中了

参考文档