◆

TypeScript tsconfig skipLibCheck 跳过声明文件检查加速编译

2023-02-02 01:370

skipLibCheck 作用

跳过对声明文件 (扩展名为 .d.ts 的文件) 的类型检查. 当程序包含了大的声明文件时, 编译器会花掉很多时间对这些已知没有错误的声明进行类型检查, 跳过这些声明文件的类型检查能够显著缩短编译时间.

还有一点就是有些库提供的 .d.ts 的类型文件中可能有问题,或者两个不同的库提供了相同的类型等,这种导致的报错是没有必要的, 所以开启 skipLibCheck:true 是一个不错的选择

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

现在几乎所有脚手架(create-vite、create-next-app、create-react-app)生成的 tsconfig.json 里都默认开着它,说明这基本是社区共识。

它跳过的是什么,没跳过的是什么

这里有个容易误解的地方:skipLibCheck 不会让 TypeScript 忽略声明文件里的类型。类型定义照样被读取、照样参与你自己代码的类型检查。它跳过的只是「对 .d.ts 文件本身做内部一致性检查」这一步。

举个例子,某个库的 index.d.ts 里写了:

// 这个类型引用了一个不存在的类型
export declare function foo(): NonExistentType;

开着 skipLibCheck 时,tsc 不会报 Cannot find name 'NonExistentType'。但如果你在自己代码里写 const x: string = foo(),这个赋值仍然会被检查(foo() 的返回类型是 any,所以这个例子通过;换成真实的类型不匹配就会报错)。

所以它的语义是「相信库作者,别帮我审他们的家庭作业」,而不是「关掉类型检查」。

真正的收益:解决无解的第三方类型冲突

性能提升在现在的项目里其实不是主要动机——TypeScript 对 .d.ts 的检查有缓存,实测大多数中小项目开关它的差别在百分之十以内。真正让它变成默认配置的是不同库的类型定义互相冲突时你根本没法修。

最经典的一类是全局类型重复声明:

node_modules/@types/node/globals.d.ts:72:11
  error TS2300: Duplicate identifier 'AbortSignal'.
node_modules/typescript/lib/lib.dom.d.ts:2119:11
  error TS2300: Duplicate identifier 'AbortSignal'.

@types/node 和 lib.dom.d.ts 都声明了 AbortSignal,两边定义还不完全一致。这不是你的代码有问题,也不是哪个包写错了,是两套环境类型硬碰。你既不能改 node_modules,也不能不装其中一个。

另一类是同一个库被装了多个版本:

error TS2345: Argument of type 'import("/app/node_modules/a/node_modules/@types/react").ReactNode'
  is not assignable to parameter of type 'import("/app/node_modules/@types/react").ReactNode'

结构一模一样但来自不同路径,TypeScript 认为是两个不同的类型。用 pnpm / yarn 的 overrides 强制版本收敛能治本,但依赖一多这个仗打不完。

第三类是库用了比你项目更新的 TS 特性。比如库的 .d.ts 里用了 const 类型参数(TS 5.0)或者 satisfies,而你的项目还在 TS 4.9,检查它的声明文件就必然报错——库的类型对它自己声明的最低 TS 版本是合法的。

这三类问题的共同点是:报错出现在 node_modules 里,你无从修改,而且它们通常不影响实际使用。skipLibCheck 就是为这种情况准备的。

代价与风险

开着它有实际代价,不是免费的:

自己项目里的 .d.ts 也会被跳过。 这是最容易忘的一点。src/types/global.d.ts、env.d.ts、shims-vue.d.ts 这些文件同样是 .d.ts,同样不再被检查。里面写错了类型名、写了非法语法(比如在 .d.ts 里放了实现代码),都不会报出来,等到别处用到才炸。

我为此改过一次习惯:项目自己的类型定义尽量写在 .ts 文件里用 export type 导出,只有确实需要全局声明(declare module、declare global)时才用 .d.ts,并且写完手动 tsc --noEmit --skipLibCheck false 单独验一遍。

库的类型 bug 会被推迟发现。 声明文件里的错误不报了,但类型信息可能已经是坏的(比如某个类型被推成 any),你写的代码基于这个坏类型通过检查,运行时才出问题。

升级依赖时的破坏性变更更难察觉。 库改了类型定义引发的不兼容,本来可以在 lib check 阶段暴露一部分。

更精准的替代方案

不想全局关掉的话,有几个更细的手段。

用 paths 把某个包的类型指向一份修好的本地声明:

{
  "compilerOptions": {
    "paths": {
      "broken-lib": ["./src/types/broken-lib.d.ts"]
    }
  }
}

用 types 收窄自动引入的全局类型包。 默认 tsc 会把 node_modules/@types 下所有包都当全局类型加载,显式列出需要的能减少冲突面:

{
  "compilerOptions": {
    "types": ["node", "vite/client"]
  }
}

这条对解决 @types/jest 和 @types/mocha 的 describe / it 重复声明特别有效。

用 exclude 或 skipDefaultLibCheck。 后者只跳过 TypeScript 自带的 lib.*.d.ts,不跳过第三方,粒度比 skipLibCheck 细一档。实际用得少,因为冲突多半来自第三方而非内置 lib。

用包管理器统一版本。 针对多版本冲突这一类,pnpm 的 overrides 是治本方案:

{
  "pnpm": {
    "overrides": {
      "@types/react": "18.2.0"
    }
  }
}

我的实际取法

新项目开 skipLibCheck: true,因为不开的话大概率一上手就被一堆 node_modules 报错拦住,成本远大于收益。同时做两件事补偿:项目自己的 .d.ts 数量压到最少,以及 CI 里加一条允许失败的 tsc --noEmit --skipLibCheck false 任务,只做告警不阻塞——这样依赖的类型问题仍然可见,但不会卡住日常开发。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVETypeScript tsconfig skipLibCheck 跳过声明文件检查加速编译加载中