# 🤥 LintStaged x TSC
我相信大多数人是不太了解 tsc
。tsc
本身是一个编译工具,它主要工作是将 .ts
转换为 .js
文件,但是大多数项目在打包时都会用 babel
来处理了,所以也就没 tsc
什么事了,大家也很少会用到。
回到我们项目,刚刚的配置为什么有问题呢?
module.exports = {
'**/*.{ts,tsx}': [
"tsc", // 检查 TypeScript
"eslint --cache --fix",
],
'**/*.{js,jsx}': [
"eslint --cache --fix",
],
"**/*.vue": [
"eslint --cache --fix",
],
"**/*.{css,less}": [
"stylelint --cache --fix",
]
}
# tsc 的参数
这里单单一个 tsc
是不够的,因为我们需要的是只检查类型,但不输出,所以要加一个 --noEmit
参数,同时也不要去检查 node_modules 里的类型,要加 --skipLibCheck
参数。完整的命令为 tsc --noEmit --skipLibCheck
。
module.exports = {
'**/*.{ts,tsx}': [
"tsc --noEmit --skipLibCheck", // 检查 TypeScript
"eslint --cache --fix",
],
...
}
但是如果我们有这样的 .ts
文件:
// messyTS.ts
const hello: Hello = {
name: 'hi'
}
以及对应的 .d.ts
类型声明文件:
// messyTypes.d.ts
interface Hello {
name: string;
}
然后我们 只在 messyTS.ts
做了改动并提交, 这条命令在 lint-staged
调用时会报下面的错误:
报错里说的是找不到 Hello
这个 interface。但是我们在写项目的时候,IDE 都会自动找到这个类型声明文件的呀,为什么这样就不行了呢?
这是因为 IDE 会自动读取读 tsconfig.json
文件,而这里 tsc
命令没有读取 tsconfig.json
导致找不到 Hello
这个 interface。那么,很自然我们就会想是否可以 tsc -p tsconfig.json --noEmit --skipLibCheck
这样写呢?抱歉,依然报错:
他奶奶地!为什么会报错?!
这是因为 tsc
只有两种调用方式:
tsc -p tsconfig.json
:直接加载tsconfig.json
时,会编译tsconfig.json
里include
的文件tsc xxx.ts
:直接编译命令行里写的 TS 文件,但是会自动忽略tsconfig.json
这里因为 lint-staged
会把提交的文件作为参数传给 tsc
命令,实际执行的命令是 tsc xxx.ts -p tsconfig.json --noEmit --skipLibCheck
,所以就会出现又要加载 tsconfig.json
编译 include
的 TS 文件,又要单独编译 **/*.ts
的文件,tsc
就蒙圈了。
这个问题也在 lint-staged
的 这个 Issue: Allow tsconfig.json when input files are specified (opens new window) 中有提到。里面对如何解决这样的冲突讨论的非常激烈。其中有一位大哥想了一个方法:我把 tsconfig.json 的 JSON 拿出来,再把里面的 key-value 对转化成 --xxx 的 bash 参数不就算加载了 tsconfig.json 了么?最后,他造了一个轮子 tsc-files (opens new window)。
# tsc-files 的问题
然而问题依然存在,因为我们一般在 tsconfig.json
里都会把 src
放在 include
里:
{
"include": ["src"],
"exclude": ["node_modules"]
}
这样一来,运行 tsc-files --noEmit
就会扫描整个 src
的 .ts
文件,无法达到 lint-staged
的目的了。
所以 tsc-files
在 v1.1.3
这个版本会把 include
设置成空数组 []
,然后把 lint-staged
的文件放在 files: ["xxx.ts"]
。
但是这又回到刚刚无法检测 messyTypes.d.ts
里 Hello
interface 的问题,因为 messyTypes.d.ts
没有被放到 files
中:
这个问题在 这个 Issue: Current version incorrectly analyzes @types/node (opens new window) 中又又又被疯狂讨论。
里面提出了一个想法:把 typeRoots
的路径放到 include
里,这样就可以用 typeRoots
自定义类型声明文件的路径来检测所有的 .d.ts
了,但是这还是有问题,具体看下面这段:
deanolium 的观点是:如果把
typeRoots
放在include
里,我们不能保证所有人都会用tsconfig.json
里的typeRoots
,因为不是所有人都是配置大神。 如果要在typeRoots
里写自定义类型声明文件目录,那就要手动加上./node_modules/@types
目录,不然不会自动 import node_modules 里的.d.ts
。 而且如果大家不了解tsc-files
的原理和实现,根本就不知道有这个坑。tsc-files
升级版本后还需要用户手动去改tsconfig.json
并不是一个好的实践。
gustavopch(作者)的观点是:一方面使用
tsc-files
时不应该加上所有的文件,因为这会扫描整个项目,就违反lint-staged
使用的初衷了。 另一方面就算include
里能读取typeRoots
目录也不能保证能自动检测到所有类型,因为有的人可能会在.ts
也用declare
来定义,也会有坑。
累了,毁灭吧。
# 我的方案
总的来说,要么扫描 src
里的所有 .ts
做类型检查,要么只扫描 Git 提交的文件,但是会报找不到类型的错误。
很抱歉,目前我能找到的资料都没有很好的解决方案,如果你有更好的 LintStaged x TypeScript 配置方案,可以 提 Issue (opens new window)。
不过我自己也想到了一个方法就是显式扫描 .d.ts
。
const declarationFiles = [
'./src/messyTypesInfo.d.ts'
]
module.exports = {
'**/*.{ts,tsx}': [
(filenames) => {
const files = [...filenames, ...declarationFiles];
return `tsc-files ${files.join(' ')} --noEmit --skipLibCheck`;
},
"eslint --cache --fix",
],
'**/*.{js,jsx}': [
"eslint --cache --fix",
],
"**/*.vue": [
"eslint --cache --fix",
],
"**/*.{css,less}": [
"stylelint --cache --fix",
]
}
或者用 fs
模块来读取项目中 ./src/typings
下的所有 .d.ts
声明文件,然后再放到命令中。
要么也可以在每次 Commit 前全面扫描:
module.exports = {
"**/*.{ts,tsx}": [
() => "tsc -p tsconfig.json --noEmit",
"eslint --cache --fix",
],
"**/*.{js,jsx}": [
"eslint --cache --fix",
],
"**/*.vue": [
"eslint --cache --fix",
],
"**/*.{css,less}": ["stylelint --cache --fix"],
};
缺点是 pre-commit 的时候会慢一点。