TypeScript 可以简单理解为类型能力 +编译能力 ,关于类型能力的文章多如牛毛,但是对其编译能力的解释却不多,本文将试图讲清楚如何配置 tsconfig.json
。
说到编译,首先就会想到 Webpack,这个配置过程一定是痛苦的,首先从它浩如烟海的配置项中找到你需要的那个配置,以及在文档里查到这个配置可能的取值,这些取值每个的作用分别是什么,是否要和其它配置组合使用,是否会影响其它配置的表现等等。
其实,所有能够提供编译能力的工具都是类似的,它们必然需要一定数量的配置才能满足使用者面临的各个场景,TypeScript 同样如此。
严格来说,TypeScript 提供的编译能力和 Webpack 并不是一个维度的 ,它只能进行语法降级 和类型定义 的生成,而不能实现代码压缩,代码分割, Tree Shaking(移除未使用到的代码)以及 Plugin 体系等。
但即便如此,TypeScript 也提供了相当数量的配置项。下面将一一介绍 TypeScript 中使用相对高频的一系列配置项。
按照这些配置的能力来划分,可以分为产物控制 、输入与输出控制 、类型声明 、代码检查几大类。
产物控制
这部分是配置中最频繁的部分,主要是 target
与 module
这两个配置项,它们分别控制产物语法的 ES 版本以及使用的模块(CommonJs / ES Module),来看看同一段代码在这两个配置组合下会被编译成什么样子,以此来更直观地了解它们的作用:
js
const arr = [1, 2, 3];
for (let i of arr) {
console.log(i);
}
const obj = {
a: 1,
b: 2,
c: 3
};
for (let key in obj) {
console.log(key);
}
这段代码在 target ES6 和 target ES5 下的编译产物如下:
js
// ES6
"use strict";
const arr = [1, 2, 3];
for (let i of arr) {
console.log(i);
}
const obj = {
a: 1,
b: 2,
c: 3
};
for (let key in obj) {
console.log(key);
}
// ES5
"use strict";
var arr = [1, 2, 3];
for (var _i = 0, arr_1 = arr; _i < arr_1.length; _i++) {
var i = arr_1[_i];
console.log(i);
}
var obj = {
a: 1,
b: 2,
c: 3
};
for (var key in obj) {
console.log(key);
}
需要注意的是,如果我们的 target 指定了一个版本,比如 es5,但你又希望使用 es6 中才有的 Promise 语法,此时就需要在 lib 配置项中新增 es2015.promise
,来告诉 TypeScript 你的目标环境中需要启用这个能力,否则就会报如下这段错误:异步函数或方法必须返回 "Promise"。请确保具有对 "Promise" 的声明或在 "--lib" 选项中包含了 "ES2015"。
js
const handler = async () => {};
js
{
"compilerOptions": {
"lib": ["ES2015"],
"target": "ES5"
}
}
除了 target 以外,module 配置项也会造成编译产物的差异:
js
export const foo = "foo";
export function bar() {
console.log("bar");
}
// module 配置为 CommonJs
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = exports.foo = void 0;
exports.foo = "foo";
function bar() {
console.log("bar");
}
exports.bar = bar;
// module 配置为 ESNext
export const foo = "foo";
export function bar() {
console.log("bar");
}
输入与输出控制
TypeScript 怎么知道哪些代码是需要进行编译处理的?输出的文件放到哪里?
在 TypeScript 中,我们首先使用 include 和 exclude 这两个配置项来确定要包括哪些代码文件,再通过 outDir 选项配置你要存放输出文件的文件夹,比如你可以这么配置:
js
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "dist",
"strict": true
},
"include": [
"src/**/*"
],
"exclude": [
"src/generated",
"**/*.spec.ts"
]
}
首先通过 include ,我们指定了要包括 src 目录下所有的文件,再通过 exclude 选项,剔除掉已经被 include 进去的文件,包括 src/generated
文件夹,以及所有 .spec.ts
后缀的测试用例文件。然后在完成编译后,你就可以在 dist 目录下找到编译产物了。
另外,files
也可以配置那些文件需要编译。一般用于文件较少的情况,如果需要使用 glob 匹配的话,则可以考虑使用 include 选项。
我们知道,TypeScript 会加载所有 node_modules 中 所有 @types 文件夹下的声明文件,假设我们的项目中被三方依赖安装了大量的 @types 文件,导致类型加载缓慢或者冲突,此时就可以使用 types 配置项来显示指定你需要加载的类型定义:
js
{
"compilerOptions": {
"types": ["node", "jest", "react"],
}
}
以上配置会加载 @types/node
,@types/jest
,@types/react
这几个类型定义包。
再来看下paths
配置,它专门来设置别名的。
js
{
"compilerOptions": {
"paths": {
"@/*": [
"src/*"
]
}
}
}
类型声明
首先是 declaration
,它的作用就一个:控制是否生成 .d.ts
文件,如果禁用的话你的编译产物将只包含 JS 文件。
与之相对的是 emitDeclarationOnly
,如果启用,则只会生成 .d.ts
文件,而不会生成 JS 文件。
如果你两个都不想要呢?请使用 noEmit
,启用后将不会输出 JS 文件与声明文件,但类型检查能力还是能保留的。
这些配置有啥用呢?
如果你的项目只使用 TS,那可能确实没用。
但很多时候,为了追求编译时的性能优化,我们可能会将 tsc 和其它编译工具组合在一起。比如,使用 Webpack 进行语法降级,只是使用 TS 来生成类型声明文件(此时就可以启用 emitDeclarationOnly),或进行类型检查(此时启用 noEmit)等等。
比如,你用vue
的脚手架创建的项目,在package.json
中有个type-check
命令,里面有带有noEmit
表示不会输出任何文件。
js
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
}
需要注意的是,如果设置了 noEmit
为 true
,同时设置了其他与输出文件相关的选项(如 outDir
、outFile
、declaration
、emitDeclarationOnly
等),那么 TypeScript 编译器会抛出一个错误,因为要生成输出文件。
代码检查
检查相关的配置,就是以 no-XXX
格式的规则,这里简要介绍下其中主要的部分:
-
noImplicitAny
,当 TypeScript 无法推断出你这个变量或者参数到底是什么类型时,它只能默默给一个 any 类型。如果你的项目维护地还比较认真,可以启用这个配置,来检查看看代码里有没有什么地方是遗漏了类型标注的。 -
noUnusedLocals
与noUnusedParameters
,类似于 ESLint 中的no-unused-var
,它会检查你的代码中是否有声明了但没有被使用的变量/函数。是否开启同样取决于你对项目质量的要求,毕竟正常情况下项目中其实不应该出现定义了但没有消费的变量,这可能就意味着哪里的逻辑出错了。在开发环境可以关闭,毕竟经常要修改。
其实有一个一键开启所有检查的配置,即 strict
配置,只要开启了该配置,其中几个以 strict
开头的配置以及 no
开头的配置都会跟随开启。
js
/* 严格的类型检查 */
"strict": true, // 启用所有严格类型检查
"noImplicitAny": true, // 不允许隐式的 any 类型
"strictNullChecks": true, // 不允许把 null、undefined 赋值给其他类型变量
"strictFunctionTypes": true, // 严格检查函数的类型
"strictBindCallApply": true, // 严格检查 bind、call 和 apply 的参数规则
"strictPropertyInitialization": true, // 类的实例属性必须初始化
"noImplicitThis": true, // 不允许 this 有隐式的 any类型
"noUnusedLocals": true, // 检查未使用的局部变量
"noUnusedParameters": true, // 检查未使用的参数
"noImplicitReturns": true, // 每个分支都会有返回值
"noFallthroughCasesInSwitch": true, // 检查 switch 语句包含正确的 break
所以,只需要配置一个 strict
就可以了,这样就可以对 any、this、空值等等类型进行检查。
总结
在日常开发中,主要的配置涉及到如下方面:
-
产物的控制: 用来确定输出的语法及模块格式。不过,在开发web项目时,ts只起一个类型检查的作用,编译的活全部交给
webpack/vite
第三方打包工具了。 -
输入与输出的控制:用来控制哪些文件需要编译处理。
-
类型声明:用来确定是否生成
js
和.d.ts
文件。 -
代码检查:主要用来设置是否代码编写规范的。