优雅的配置你的 tsconfig.json

TypeScript 可以简单理解为类型能力 +编译能力 ,关于类型能力的文章多如牛毛,但是对其编译能力的解释却不多,本文将试图讲清楚如何配置 tsconfig.json

说到编译,首先就会想到 Webpack,这个配置过程一定是痛苦的,首先从它浩如烟海的配置项中找到你需要的那个配置,以及在文档里查到这个配置可能的取值,这些取值每个的作用分别是什么,是否要和其它配置组合使用,是否会影响其它配置的表现等等。

其实,所有能够提供编译能力的工具都是类似的,它们必然需要一定数量的配置才能满足使用者面临的各个场景,TypeScript 同样如此。

严格来说,TypeScript 提供的编译能力和 Webpack 并不是一个维度的 ,它只能进行语法降级类型定义 的生成,而不能实现代码压缩,代码分割, Tree Shaking(移除未使用到的代码)以及 Plugin 体系等。

但即便如此,TypeScript 也提供了相当数量的配置项。下面将一一介绍 TypeScript 中使用相对高频的一系列配置项。

按照这些配置的能力来划分,可以分为产物控制输入与输出控制类型声明代码检查几大类。

产物控制

这部分是配置中最频繁的部分,主要是 targetmodule 这两个配置项,它们分别控制产物语法的 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"
  }

需要注意的是,如果设置了 noEmittrue,同时设置了其他与输出文件相关的选项(如 outDiroutFiledeclarationemitDeclarationOnly 等),那么 TypeScript 编译器会抛出一个错误,因为要生成输出文件。

代码检查

检查相关的配置,就是以 no-XXX 格式的规则,这里简要介绍下其中主要的部分:

  • noImplicitAny,当 TypeScript 无法推断出你这个变量或者参数到底是什么类型时,它只能默默给一个 any 类型。如果你的项目维护地还比较认真,可以启用这个配置,来检查看看代码里有没有什么地方是遗漏了类型标注的。

  • noUnusedLocalsnoUnusedParameters,类似于 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文件。

  • 代码检查:主要用来设置是否代码编写规范的。

相关推荐
Amd7945 小时前
Nuxt.js 应用中的 prepare:types 事件钩子详解
typescript·自定义·配置·nuxt·构建·钩子·类型
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
鸿蒙开天组●1 天前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70
zhizhiqiuya1 天前
第二章 TypeScript 函数详解
前端·javascript·typescript
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
_jiang2 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
清清ww2 天前
【TS】九天学会TS语法---计划篇
前端·typescript
努力变厉害的小超超3 天前
TypeScript中的类型注解、Interface接口、泛型
javascript·typescript
王解4 天前
Jest进阶知识:整合 TypeScript - 提升单元测试的类型安全与可靠性
前端·javascript·typescript·单元测试