likes
comments
collection
share

JS->TS 渐进式类型化

作者站长头像
站长
· 阅读数 20

现在几乎已经没有人没有接触过 TS,但业内对于 TS 的态度两极分化。喜欢的恨不得全用上,讨厌的公开表示不想再看到它。

我属于前者,但我也知道 TS 确实有很多令人讨厌的地方,我尝试去解决或者去淡化它所带来的不好的影响。这篇文章分享的就是我所做的一部分尝试。

场景

相信刚接触 TS 的同学都会疑惑一件事:怎么将通篇 JS 的代码改造成 TS?

事实上,这个问题并不好解决,并不是把 .js 全局重命名为 .ts,就能解决的。

由于 TS 的能力建立在类型之上,缺少完善的类型,没法发挥它最大的优势,和缺点(划掉)

TS 的一个核心能力就是类型纠错,检查越严格,越能享受它带来的好处。

但我们的老代码,并不是立刻就能把所有类型给完善掉的。业务代码最需要类型提示,但它类型来自各种各样依赖的包,得从底层改起。改过的同学都知道,一旦把 .js 改成 .ts 满屏都是飘红的 ts-error,改造进度遥遥无期,简直就是无底洞。

JS->TS 渐进式类型化

【令人窒息的类型.jpg】

TS 提供了 tsc 做编译和类型检查,但由于tsconfigexclude 配置只能阻止主动检查。如果其他文件用到了 exclude 指定范围的文件,还是会被 TS 扫到并抛出类型错误。

因此就出现了一个困局:

「我想用 TS啊,但我得先解决文件里的类型覆盖问题啊。我想加类型覆盖啊,但是一环套一环,我改不完啊。」

解决方案

在 2019 年刚接触 TS 的时候,我也遇到过这个问题。跟业内其他用 TS 的同行聊过解决方案,也没有得到什么好的解决方案,只能先把 allowJs打开,然后慢慢改。

不过我觉得,要是项目复杂到只能用 allowJs,那还不如用 JSDoc

后面随着 TS 在项目中的实践越来越深,机缘巧合之下,我意识到其实可以调用 TS 的 API 去做检查,然后自己做一道筛选,再输出就可以了。

不过网上关于 TypeScript 二次编程相关的案例很少,我没能在网上找到手把手入门的教程,只能用着我英语4级低空飘过的渣渣水平,去硬着头皮啃 TS github 仓库下官方提供的 Wiki 文档。

好在 Wiki 上提供了一个很接地气的案例:Using-the-Compiler-API

 import * as ts from "typescript";
 ​
 function compile(fileNames: string[], options: ts.CompilerOptions): void {
   let program = ts.createProgram(fileNames, options);
   let emitResult = program.emit();
 ​
   let allDiagnostics = ts
     .getPreEmitDiagnostics(program)
     .concat(emitResult.diagnostics);
 ​
   allDiagnostics.forEach(diagnostic => {
     if (diagnostic.file) {
       let { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start!);
       let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
       console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
     } else {
       console.log(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
     }
   });
 ​
   let exitCode = emitResult.emitSkipped ? 1 : 0;
   console.log(`Process exiting with code '${exitCode}'.`);
   process.exit(exitCode);
 }
 ​
 compile(process.argv.slice(2), {
   noEmitOnError: true,
   noImplicitAny: true,
   target: ts.ScriptTarget.ES5,
   module: ts.ModuleKind.CommonJS
 });

其中第5行的program.emit(),就是使用代码运行 program.emit(),后面拿到 diagnostic 里的错误信息,就可以按照自己的意思来输出日志。

顺着这个思路,最后实现了我的构想。

ts-exactly-check

我编写了一个 npm 包,名叫 ts-exactly-check,封装了这套流程,用来帮助我在自己各个项目里复用这种渐进式改造的方式。它通过一份配置文件,决定 TypeScript 如何进行编译。

它支持如下配置:

 interface TSCheckConfig {
   /** 全局 .d.ts 文件的依赖 */
   types?: string[];
   /** 忽略的规则(glob 格式) */
   exclude?: string[];
   /** 需要检查的文件(glob 格式) */
   include?: string[];
   /** 忽略的文件(这里是和 include 配合使用的,可以忽略里面的某几个文件) */
   ignore?: string[];
   /** 和 ignore 作用一样,语义上用来标记暂时不检查,但后续需要完善类型的文件 */
   todo?: string[];
 }

这个工具读取的是项目根目录下的tscheck.config.ts文件,上面提到的配置也是写在这个文件当中。

大部分情况下,只要配置 includeexclude 就可以满足需求了。

举个栗子。有这样一个目录结构的项目:

 ├── src
 │   ├── index.ts
 │   └── lib.ts
 └── tscheck.config.ts

其中 src/lib.ts 里面有个还没改造完的 tsError,使用 tsc 输出如下:

JS->TS 渐进式类型化

如果我想忽略这个文件里的错误,下次再改,可以配置:

 // tscheck.config.ts
 module.exports = {
   include: ["./src/**/*"], // 指定检查范围
   exclude: ["src/lib.ts"], // 指定忽略范围
 };

使用 ts-exactly-check 内置的命令ts-check,得到如下结果:

JS->TS 渐进式类型化

做实验要严谨。这里给出不配置 exclude 时的输出:

JS->TS 渐进式类型化

与其他工具配合

在 ts-exactly-check 里面,我使用 process.exit结束输出,因此可以和其他工具配合。

比如 git,使用 husky,可以在 push 阶段检查类型。如果出现 ts-error,可以中断 push。

比如 ci,可以在流水线触发编译的时候,进行类型检查。如果出现 ts-error,可以中断流水线,抛出异常。

如果 ci 不识别 node 的异常流程,无法中断,可以通过这么一个小脚本去控制 ci 的中断:

#!/bin/bash
function checkErrorCode(){
   echo "errorCode: $1"
   if [ $1 != 0 ]; then
      echo "脚本未通过" && exit 42
   fi
}
npm run ts-check
checkErrorCode $? # 检查错误码,如果非 0 就 exit 42

最后

这套方案我已经实践使用了半年了,使用的是 10w+行 的项目,指定检查 500 个文件的时候,耗时 60s 以内。

现在将它的核心部分放出来出来,顺便升到 TS5,给大家使用和借鉴。希望可以帮助到社区上的同学们。

(项目内)安装方式:npm i ts-exactly-check -D

(项目内)使用方式:npx ts-check