Skip to content

概述

tsconfig.json 文件是用于描述将 TypeScript 转为 JavaScript 代码的配置文件。

IDE(代码编辑器)将会根据 tsconfig.json 文件来对当前项目中支持不同程度的类型约束,同时也是对 TSC 编译 TypeScript 代码过程做一些预定义、约束入口和编译输出目录等配置。

因此对于一个支持 TypeScript 编程语言的工程来说,tsconfig.json 文件就是编码的基础。

什么是 tsc

tsc 的全称是 TypeScript Compiler,也就是将 TypeScript 转码为 JavaScript 代码的编译器。

tsc 的全局安装方式:

bash
npm install typescript -g

当我们编译一份 index.ts 文件时,会使用下面的命令:

bash
tsc ./index.ts

这样就可以得到一份编译成为 JavaScript 代码的 ./index.js 文件。

tsc 实际就是将 TS 转为 JS 的编译(器)脚手架工具,如果是一个 TS 的前端工程项目,那么就可以通过项目中的 tsconfig.json 文件来自定义配置 TS 编译相关规则。

项目中的 tsconfig.json 文件,我们一般会通过如下快捷命令生成:

bash
tsc --init

执行完后,会在项目根目录生成一个简单的初始化 tsconfig.json 配置描述文件,如果没有特别的要求,该初始化配置就足以支持你愉快地使用 TS 开发啦!

更多相关 TS 编译配置和使用说明可以通过 tsc -h 查看。

配置详解

files

files 字段用于指明需要 tsc 编译的一个或多个 ts 文件,例如:

json
{
  "files": ["index.ts", "global.d.ts"],
}

当指定的文件或文件夹不存在时,会提示 ❌ 错误!

include

include 字段用于指明需要被 tsc 编译的文件或文件夹列表,例如:

json
{
    "include": [
        "src",
        "global.d.ts"
    ],
}

exclude

exclude 字段用于排除不需要 tsc 编译的文件或文件夹列表,例如:

json
{
    "exclude": ["test.ts", "src/test.ts"],
}

注意exclude 字段中的声明只对 include 字段有排除效果,对 files 字段无影响,即与 include 字段中的值互斥。

如果 tsconfig.json 文件中 filesinclude 字段都不存在,则默认包含 tsconfig.json 文件所在目录及子目录的所有文件,且排除在 exclude 字段中声明的文件或文件夹。

compileOnSave

compileOnSave 是声明是否需要在保存时候自动触发 tsc 编译的字段,一般来说,我们的代码编译过程会通过 Rollup、Webpack 等打包构建工具,并且使用热更新,因此无需配置该项,保持缺省即可。

json
{
    "compileOnSave": false,
}

extends

extends 字段用于指明继承已有的 tsconfig 配置规则文件。

该字段可以说是非常有用了,因为我们的 tsconfig 配置其实各个项目之间大同小异,因此完全可以结合自己团队的情况,抽离一个基础且公共的 tsconfig 配置,并将其发包,然后作为 extends 字段的值来继承配置。

tsconfig 推荐默认配置可以参考官方的包:@tsconfig/recommended

@tsconfig/recommended 的配置如下:

json
{
    "compilerOptions": {
        "target": "ES2015",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
    },
    "$schema": "https://json.schemastore.org/tsconfig",
    "display": "Recommended"
}

例如继承一个发包后的 tsconfig 基础配置,并通过显示声明编译的目标代码版本为 ES2016 来覆盖覆盖 @tsconfig/recommended 中对应配置项。

json
{
    "extends": "@tsconfig/recommended/tsconfig.json",
    "compilerOptions": {
        "target": "ES2016"
    }
}

作为一些实践经验,社区也提供了一些常见环境(例如:Nuxt、Vite、Node 等)最佳实践后的基础配置,推荐参阅:https://github.com/tsconfig/bases/

compilerOptions

compilerOptions 是一个描述 TypeScript 编译器功能的「大」字段,其值类型是「对象」,因此包含了很多用于描述编译器功能的子字段,其子字段的功能如下:

target

target 字段指明经过 TSC 编译后的 ECMAScript 代码语法版本,根据 ECMAScript 语法标准,默认值为 ES3

TypeScript 是 JavaScript 的超集,是对 JavaScript 语法和类型上的扩展,因此我们可以使用 ES5、ES6,甚至是最新的 ESNext 语法来编写 TS。例如当我们使用 ES2021 语法来编码 TS 文件,同时配置如下:

json
{
    "compilerOptions": {
        "target": "ES5",
    }
}

则会将对应使用了最新 ECMAScript 语法的 TS 文件编译为符合 ES5 语法规范的 *.js 文件。

注意:tsc 的代码降级编译并不能完全处理兼容性,可通过本地自定义 Demo 编译得出结论。

lib

lib 字段是用于为了在我们的代码中显示的指明需要支持的 ECMAScript 语法或环境对应的类型声明文件。

例如我们的代码会使用到浏览器中的一些对象 windowdocument,这些全局对象 API 对于 TypeScript Complier 来说是不能识别的:

lib 未显示引入 DOM 会提示类型错误

因而需要在 lib 字段中如下配置:

json
{
    "compilerOptions": {
        "target": "ES5",
        "lib": ["ES5", "ES6", "DOM"],
    }
}

来显式引入在 DOM 即浏览器环境下的一些默认类型定义,即可在代码中使用,windowdocument 等浏览器环境中的对象,TS 在运行时以及编译时就不会报类型错误。

综合 targetlib 字段的实际功能表现,我们可以得出结论:

TSC 的编译结果只有部分特性做了 pollyfill 处理,ES6 的一些特性仍然被保留,想要支持完全的降级到 ES5 还是需要额外引入 pollyfill(也就是我们在项目的入口文件处 import 'core-js'),但建议是将 target 字段值设置为 ES6,提升 TSC 的速度。

因此,对于使用 TSC 编译我们需明确:不应该将 TSC 作为编译项目的工具,应该将 TSC 作为类型检查工具,代码编译的工作尽量交给 Rollup、Webpack 或 Babel 等打包工具。

module

module 字段指明 tsc 编译后的代码应该符合何种「模块化方案」,可以指定的枚举值有:none, commonjs, amd, system, umd, es2015, es2020, 或 ESNext,默认值为 none

在如今的前端开发趋势来讲,主要是使用 ESM、CommonJS、UMD、IIFE 四种模块化方案,未来会趋向于 ESM,当然我们会根据项目的应用场景来决定使用何种模块化方案,例如:NodeJS 使用 CommonJS,浏览器里可以使用 ESM,不过现在的打包工具,会自动处理 CommonJS 和 ESM 的差异,并包装成符合指定模块化规范的代码,

tsconfig.json 可以设置 allowSyntheticDefaultImports 字段为 true,来允许合成默认导入。

esModuleInterop

简单来说,就是支持合成默认导入。

在前端项目开发时,使用 ESM 编写代码引入了 CJS 的模块,由于 CJS 模块没有默认导出内容,因此需要通过我们的工具去自动化合成 CJS 的默认导出,以支持在 ESM 下流畅开发。

参阅文章《esModuleInterop 到底做了什么?》,讲得非常详细也非常好。

esModuleInterop 字段设置为 true 时候,上述提到的 allowSyntheticDefaultImports 字段也会自动设置为 true

moduleResolution

moduleResolution 声明如何处理模块,枚举值:classicnode,会根据 module 字段决定默认值。

推荐手动设置为 node,更符合现在大家的编码认识一些,而且大部分的构建打包工具都是基于 Node。

举个🌰,遇到 import { a } from 'a-lib'; 这样的模块引入代码应该如何去(解析)查找到对应的模块文件。

baseUrl & paths

  • baseUrl: 设置基本目录以解析非绝对模块名称(定义一个根目录,以此进行绝对文件路径解析)
  • paths: 用于设置模块名或路径映射列表,这样就可以简写项目中自定义模块的文件路径。

举一个 🌰:

ts
// tsconfig.json
{
    "compilerOptions": {
        // 注意:baseUrl 必选,与 paths 成对出现,以 tsconfig.json 文件所在目录开始
        "baseUrl": ".",
        "paths": {
            // 映射列表
            "@/*": [
                "src/*"
            ],
            "moduleA": [
                "src/libs/moduleA"
            ]
        }
    }
}

// 代码里这么写
import Toast from '@/components/Toast.ts' // 模块实际位置: src/components/Toast.ts
import TestModule from 'moduleA/index.js' // 模块实际位置: src/libs/moduleA/index.js

注意:如果需要自动生成(导出)类型定义文件,TSC 不会处理路径别名,需要引入 typescript-transform-paths 插件,以及 TTypescript 来转换路径别名为相对路径。

由于当前的 TypeScript 不支持 tsconfig.json 中的自定义转换器,且无法使用 tsc 命令使用自定义转换器编译文件,所以引入了 TTypescript 作为包装器

json
// tsconfig.json
{
    "compilerOptions": {
        "baseUrl": "./",
        // 配置路径别名映射
        "paths": {
            "@/*": ["src/*"]
        },
        "plugins": [
            // 转换输出 js 文件中的路径
            { "transform": "typescript-transform-paths" },

            // 转换输出 .d.ts 文件中的路径
            { "transform": "typescript-transform-paths", "afterDeclarations": true }
        ]
    }
}

plugins 是用于扩展 TSC 编译器功能的字段。

例如在 Rollup 打包环境下,可以如下配置:

ts
import typescript from '@rollup/plugin-typescript';
import ttypescript from 'ttypescript';

export default [
    {
        input: './src/index.ts',
        output: {
            dir: 'dist',
            format: 'cjs',
            entryFileNames: 'index.js',
        },
        plugins: [
            typescript({
                typescript: ttypescript,
            }),
        ],
    },
];

如果是有自动导出类型定义文件的需求,才需要搞这一套插件~

rootDir & outDir

  • rootDir: 指定 TypeScript 识别读取的根目录,用于所有非声明输入文件的最长公共路径 例如:'"rootDir": "./src",则 src 目录下的 TS 文件不能引用 src 目录以外的 ts 文件,一般我们会设置为 ./src./(即 tsconfig.json 所在目录)

  • outDir: 输出目录,即 tsc 编译后的文件输出的文件夹路径(基于 tsconfig.json 文件的相对路径) 例如:"outDir": "./dist",即将 TSC 编译输出的 JS 文件,统一输出的 ./dist 目录下。

jsx

如果是有 jsx 语法需要支持的项目,可以设置值 preservereact 等:

json
{
    "compilerOptions": {
        "jsx": "preserve", // 一般 preserve 即可
    },
}

importHelpers

importHelpers 决定是否启用从 tslib 库引入语法降级辅助函数,以避免重复冗余的辅助函数声明。

个人建议是设置为 true 来启用。

experimentalDecorators

experimentalDecorators 用于声明是否启实验性用装饰器模式。

TypeScript 和 ES6 中引入了 Class 的概念,同时在 Decorators 提出了装饰器模式,通过引入装饰器模式,能极大简化书写代码。

当前对于 Decorator 的支持性不太好,如果是一些涉及到使用了装饰器的需要,就需要开启这个属性。

noEmit

noEmit 设置是否输出 js 文件,一般是设置为 false,将打包等工作交给 Webpack 等工具。

配置全解析

以上针对 tsconfig.json 中一些常见配置做了详细解释,将一些不常用的配置字段组合在一起,做一个 Checklist 如下:

json
{
    "compilerOptions": {
        /* 基本选项 */
        "target": "es6", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
        "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
        "lib": [], // 指定要包含在编译中的库文件
        "allowJs": true, // 允许编译 javascript 文件
        "checkJs": true, // 报告 javascript 文件中的错误
        "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
        "declaration": true, // 生成相应的 '.d.ts' 文件
        "declarationDir": "./dist/types", // 生成的 '.d.ts' 文件保存文件夹
        "sourceMap": true, // 生成相应的 '.map' 文件
        "outFile": "./", // 将输出文件合并为一个文件
        "outDir": "./dist", // 指定输出目录
        "rootDir": "./", // 用来控制输出目录结构 --outDir.
        "removeComments": true, // 删除编译后的所有的注释
        "noEmit": true, // 不生成输出文件
        "importHelpers": true, // 从 tslib 导入辅助工具函数
        "isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

        /* 严格的类型检查选项 */
        "strict": true, // 启用所有严格类型检查选项
        "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
        "strictNullChecks": true, // 启用严格的 null 检查
        "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
        "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

        /* 额外的检查 */
        "noUnusedLocals": true, // 有未使用的变量时,抛出错误
        "noUnusedParameters": true, // 有未使用的参数时,抛出错误
        "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
        "noFallthroughCasesInSwitch": true, // 报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿)

        /* 模块解析选项 */
        "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
        "baseUrl": "./", // 用于解析非相对模块名称的基础目录
        "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
        "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
        "typeRoots": [], // 包含类型声明的文件列表
        "types": [], // 需要包含的类型声明文件名列表
        "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
        "esModuleInterop": true, // 支持合成模块的默认导入

        /* Source Map Options */
        "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
        "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
        "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
        "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

        /* 其他选项 */
        "experimentalDecorators": true, // 启用装饰器
        "emitDecoratorMetadata": true // 为装饰器提供元数据的支持
    },
    /* 指定编译文件或排除指定编译文件 */
    "include": ["src/**/*"],
    "exclude": ["node_modules", "**/*.spec.ts"],
    "files": ["index.ts", "test.ts"],
    // 从另一个配置文件里继承配置
    "extends": "@tsconfig/recommended",
    // 让 IDE 在保存文件的时候根据 tsconfig.json 重新生成文件
    "compileOnSave": true // 支持这个特性需要 Visual Studio 2015, TypeScript 1.8.4 以上并且安装 atom-typescript 插件
}

构建集成

前文讲到了为什么不推荐直接使用 TSC 作为项目的打包编译工具,那么接下来就简单看看在常见的几款打包工具中针对 TypeScript 的编译方案是如何设计的?

Rollup + TypeScript

在 Rollup 打包中,我们一般只需要添加 @rollup/plugin-typescript 插件即可,该插件会默认读取项目根目录下的 tsconfig.json 配置文件。

Rollup 的配置就像这样:

js
// file: rollup.config.js
import typescript from '@rollup/plugin-typescript';

export default {
    input: 'src/index.ts',
    output: {
        dir: 'output',
        format: 'cjs'
    },
    plugins: [typescript()]
};

Webpack + TypeScript

Webpack 中的 TypeScript 官方文档中,指明了需要安装:typescriptts-loader 两个模块。

配置 Webpack 并支持 TypeScript 的配置如下:

js
// file: webpack.config.js
const path = require('path');

module.exports = {
    entry: './src/index.ts',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
};

可以看出 Webpack 主要是依赖 ts-loader 实现对 TypeScript 语法的编译支持,再看看对 ts-loader 的介绍:

ts-loader

换句话说,ts-loader 实际调用了 TSC 来编译 TS 文件,TSC 的配置依赖于你项目中的 tsconfig.json 文件。

如果使用了 Babel,则可以使用 @babel/preset-typescript 来处理,但 Babel 不会做 TS 类型校验,在打包工具 Rollup 和 Webpack 中都可以引入 Babel,那么接下来看看 Babel 是如何处理 TypeScript 的吧!

Babel + TypeScript

Babel 处理 TS 需要安装 @babel/preset-typescript 模块,然后在 babel 项目配置文件中声明:

js
// 配置说明:https://babeljs.io/docs/en/babel-preset-typescript
{
  "presets": ["@babel/preset-typescript"]
}

但 Babel 中只会对 TS 代码转为 JS 代码(通过 parse TS 文件为 AST,并直接移除类型信息,然后打印目标代码),不会去做 TS 类型检查,所以 Babel 编译 TS 文件相较于 TSC 的速度更快!

同时,因为 Babel 会根据不同的兼容环境,按需引入 pollyfill,比 TSC 直接引入 core-js 更优雅,因此使用了 Babel 打包的体积也会更小。

TS 类型检查工作可以交给代码编辑器承担,当然同时可以新增 TS 检查的命令:

json
// package.json
{
    "script": {
        "tsCheck": "tsc --noEmit",
    }
}

可以把类型检查放到特定的 npm scripts 生命周期之前,另外其实也可以将类型检查放到 git commit 阶段,用于做必要的 TS 类型检查,保证项目的正确性。

ESbuild + TypeScript

通过 Vite 体会到了 ESbuild 带来的开发热更新「极速」体验,针对 TS 项目,ESbuild 和 Babel 是相同的编译策略,即仅编译,不校验类型。

ESbuild 处理 TypeScript 同样可以带来飞一般的感觉!

Vite 使用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms。—— Vite Dos

但在 ESbuild 中需要启用 tsconfig 中的 isolatedModules 功能,然后在类型引入的时候需要替换,规则参考如下:

ts
// old
import { UserType } from './types';

// new
import type { UserType } from './types';

因为 ESbuild 是单独编译每个文件,无法判断引入的是 Type(类型) 还是 ,所以需要开发者显示地声明是「Type」。

同时还需要启用 esModuleInterop 功能,用于支持 ESM 模块合成默认导入,以兼容 CJS 和 ESM 规范。

另外 ESbuild 不支持:emitDecoratorMetadatconst enum 类型和 *.d.ts 文件。

此外,关注到兼容性处理这方面,Bable 和 ESbuild 是类似的,因此会存在兼容性问题:

兼容性

对于装饰器处理不支持,因为 TS 是 JS 的超集,ESnext 的规范提案某些还不是稳定的,因此如果有这方面诉求的项目,可以借助 TSC 做预编译,例如使用 Rollup 的 typescript 插件 或 Webpack 的 ts-loader 方式。

多环境配置

在一个中大型项目中,通常包括了业务实现、单元测试、构建配置等代码,如果共用一份 tsconfig.json 配置,则会在各自脚本中引入冗余的类型定义,造成类型泄露,严重影响基于 TypeScript 的开发体验(比如在 Web 项目中错误引入了 @types/node 类型库,则在开发时会出现 process 等全局变量类型错误等),那么,解决方案是什么呢?

没错,我们需要为每个环境定义一个 tsconfig.json 文件。注意,不是为每个目录,而是为你的代码运行的每个环境。

在现代 Web 应用程序的幕后,是一个精妙的模块组合。应用程序的源代码需要被编译、压缩、代码拆分、打包,并最终交付给用户。此外,还有测试文件,这些也是 TypeScript 模块,但它们从未被编译或交付给任何人。可能还有 Storybook 故事、Playwright 测试,或者一些自定义的 *.ts 脚本用于自动化任务——所有这些都是有用的,它们有着不同的意图,并且运行在不同的环境中。

最佳实践