现在几乎已经没有人没有接触过 TS,但业内对于 TS 的态度两极分化。喜欢的恨不得全用上,讨厌的公开表示不想再看到它。
我属于前者,但我也知道 TS 确实有很多令人讨厌的地方,我尝试去解决或者去淡化它所带来的不好的影响。这篇文章分享的就是我所做的一部分尝试。
场景
相信刚接触 TS 的同学都会疑惑一件事:怎么将通篇 JS 的代码改造成 TS?
事实上,这个问题并不好解决,并不是把 .js
全局重命名为 .ts
,就能解决的。
由于 TS 的能力建立在类型之上,缺少完善的类型,没法发挥它最大的优势,和缺点(划掉) 。
TS 的一个核心能力就是类型纠错,检查越严格,越能享受它带来的好处。
但我们的老代码,并不是立刻就能把所有类型给完善掉的。业务代码最需要类型提示,但它类型来自各种各样依赖的包,得从底层改起。改过的同学都知道,一旦把 .js
改成 .ts
满屏都是飘红的 ts-error,改造进度遥遥无期,简直就是无底洞。
【令人窒息的类型.jpg】
TS 提供了 tsc
做编译和类型检查,但由于tsconfig
的 exclude
配置只能阻止主动检查。如果其他文件用到了 exclude 指定范围的文件,还是会被 TS 扫到并抛出类型错误。
因此就出现了一个困局:
「我想用 TS啊,但我得先解决文件里的类型覆盖问题啊。我想加类型覆盖啊,但是一环套一环,我改不完啊。」
解决方案
在 2019 年刚接触 TS 的时候,我也遇到过这个问题。跟业内其他用 TS 的同行聊过解决方案,也没有得到什么好的解决方案,只能先把 allowJs
打开,然后慢慢改。
不过我觉得,要是项目复杂到只能用 allowJs
,那还不如用 JSDoc
。
后面随着 TS 在项目中的实践越来越深,机缘巧合之下,我意识到其实可以调用 TS 的 API 去做检查,然后自己做一道筛选,再输出就可以了。
不过网上关于 TypeScript 二次编程相关的案例很少,我没能在网上找到手把手入门的教程,只能用着我英语4级低空飘过的渣渣水平,去硬着头皮啃 TS github 仓库下官方提供的 Wiki 文档。
好在 Wiki 上提供了一个很接地气的案例:Using-the-Compiler-API。
javascript
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 如何进行编译。
它支持如下配置:
php
interface TSCheckConfig {
/** 全局 .d.ts 文件的依赖 */
types?: string[];
/** 忽略的规则(glob 格式) */
exclude?: string[];
/** 需要检查的文件(glob 格式) */
include?: string[];
/** 忽略的文件(这里是和 include 配合使用的,可以忽略里面的某几个文件) */
ignore?: string[];
/** 和 ignore 作用一样,语义上用来标记暂时不检查,但后续需要完善类型的文件 */
todo?: string[];
}
这个工具读取的是项目根目录下的tscheck.config.ts
文件,上面提到的配置也是写在这个文件当中。
大部分情况下,只要配置 include
和 exclude
就可以满足需求了。
举个栗子。有这样一个目录结构的项目:
arduino
├── src
│ ├── index.ts
│ └── lib.ts
└── tscheck.config.ts
其中 src/lib.ts
里面有个还没改造完的 tsError,使用 tsc
输出如下:
如果我想忽略这个文件里的错误,下次再改,可以配置:
java
// tscheck.config.ts
module.exports = {
include: ["./src/**/*"], // 指定检查范围
exclude: ["src/lib.ts"], // 指定忽略范围
};
使用 ts-exactly-check 内置的命令ts-check
,得到如下结果:
做实验要严谨。这里给出不配置 exclude
时的输出:
与其他工具配合
在 ts-exactly-check 里面,我使用 process.exit
结束输出,因此可以和其他工具配合。
比如 git,使用 husky
,可以在 push 阶段检查类型。如果出现 ts-error,可以中断 push。
比如 ci,可以在流水线触发编译的时候,进行类型检查。如果出现 ts-error,可以中断流水线,抛出异常。
如果 ci 不识别 node 的异常流程,无法中断,可以通过这么一个小脚本去控制 ci 的中断:
sh
#!/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