前言
由于项目近期进行 ts 迁移,作为第一个吃螃蟹的人,踩过了不少坑。迁移过程中遇到的大大小小的问题基本上都解决了,但是对于 shims-vue.d.ts 文件的命名以及其内的模块声明始终找不到比较贴切的解释。沉下心来读了些外网资料,总算是有点“豁开云雾见青天”的感觉了。此处就记录我对于 ts 全局模块声明的一些思考以及一些 ts 项目迁移遇到的坑。
Vue ts 声明文件
在安装 @vue/typescript 之后,项目会生成两个新文件,分别是 shims-vue.d.ts
和 shims-jsx.d.ts
,其内容分别是:
1 | // shims-vue.d.ts |
和
1 | import Vue, { VNode } from 'vue'; |
那么这两个文档有什么作用呢?
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.ts
和 declarations.d.ts
。前者主要维护需要写的比较详细的外部模块,后者主要维护简写模式的模块(包括内部需要声明的 .js 文件,兼容历史遗留问题)。例如:
改造后的 module/index.d.ts
1 | // This `declare module` is called ambient module, which is used to describe modules written in JavaScript. |
改造后的 module/declarations.d.ts
1 | // Shorthand ambient modules, All imports from this shorthand module will have the any type. |
附加:对于 global 声明可视情况分类,比如通用的放在
global.d.ts
,其余可视情况(如果该类型比较多的话)按照对应类型分类,比如 table 的可全部放在global-table.d.ts
。
全局声明的写法
另一个一直比较疑惑的问题是全局声明的写法,比如模块的“单文件单模块声明”的写法“单文件多模块合并声明”的写法不太一样,“无导入的全局声明文件”和“带导入声明的全局声明文件”的写法又有些不同,这里我一一列出其可行的写法以及其不同的原因。
注:这里的一些定义都是个人总结的便于记忆的说法,为非标准定义。
单文件单模块声明
该文件支持两种写法,分别如下:
1 | // 写法一 |
注: 前者(写法一)主要为无 ts 声明的模块添加声明,后者(写法二)主要为已有 types 声明的模块进行声明扩展(可以参考 vue-router 源码部分)
单文件多模块合并声明
仅有一种写法(需要关闭对应的多次引入重复模块的 lint 规则或者忽略此 types 文件夹内的所有内容)
1 | declare module '*.vue' { |
无导入的全局声明文件
无导入即没有 import 声明,直接定义全局接口、函数等
1 | interface TableRenderParam extends BasicObject { |
带导入声明的全局声明文件
带有 import 导入插件声明的必须显示定义 global,例如:
1 | import { CreateElement } from 'vue'; |
不同的原因
如果在“单文件多模块合并声明”将 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 迁移的踩坑记录中了