中高级前端必须掌握的tsconfig.json配置最新最全指南【持更】

7,435 阅读33分钟

TypeScript 配置文件(tsconfig.json)是用于配置 TypeScript 项目的重要文件。它允许开发者自定义 TypeScript 编译器的行为,指定编译选项、文件包含与排除规则、输出目录等。通过合理配置 tsconfig.json,我们可以根据项目需求进行灵活的 TypeScript 编译设置。

本文将全面解读 tsconfig.json 的各个配置选项,并提供一些常见的使用场景和示例代码

常用选项概览

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5", // 生成代码的 ECMAScript 目标版本
    "module": "commonjs", // 生成代码的模块标准
    "lib": ["es6", "dom"], // 告诉 TypeScript 编译器要包括哪些环境的类型声明文件(`.d.ts` 文件)
    "allowJs": true, // 是否编译 JS 文件
    "checkJs": true, // 是否在 JS 文件中报告错误
    "jsx": "preserve", // 在 .tsx 文件里支持 JSX: 'preserve', 'react-native', or 'react'
    "declaration": true, // 是否生成 .d.ts 类型定义文件
    "emitDeclarationOnly": true, // 只生成类型声明文件,不生成js
    "declarationMap": true, // 为每个 .d.ts 文件生成 sourcemap
    "sourceMap": true, // 是否生成 .map 文件
    "outFile": "./dist/main.js", // 将多个输出文件合并为一个文件,`module`为`None` 、 `System`或`AMD` 时可用,CommonJS 或 ES6 模块。不可用
    "outDir": "./dist", // 输出文件夹
    "rootDir": "./", // 指定 TypeScript 编译器查找 TypeScript 文件的根目录,通常用于控制输入文件的搜索路径。假设你的 TypeScript 文件存储在项目的根目录下,你可以配置为 './'
    "composite": true, // 生成额外的元数据文件,这些文件可以帮助构建工具(包括TypeScript自身的--build模式)更快地确定项目是否已经被构建。
    "removeComments": true, // 删除注释
    "noEmit": true, // 不输出文件
    "importHelpers": true, // 通过 tslib 引入辅助工具函数
    "downlevelIteration": true, // 是否添加对迭代器和生成器的补丁(es6+无需关注)
    "useDefineForClassFields": true, // 是否使用 Object.defineProperty 定义类实例属性

    /* 严格的类型检查 */
    "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

    /* 模块解析 */
    "isolatedModules": true, // 控制是否将每个文件作为单独的模块处理。
    "moduleResolution": "node", // 模块解析策略
    "allowImportingTsExtensions": true, // 允许在导入 TypeScript 文件时指定文件扩展名(如 `.ts`, `.tsx`),当导入路径需要显式包含文件扩展名时,确保兼容性
    "baseUrl": "./", // 解析使用非相对路径导入模块时的基地址
    "paths": {}, // 模块名称到基于 baseUrl 的路径映射表
    "rootDirs": [], // 将多个文件夹放在一个虚拟目录下,合并多个源文件目录
    "typeRoots": [], // typeRoots用来指定声明文件或文件夹的路径列表,如果指定了此项,则只有在这里列出的声明文件才会被加载
    "types": [], // types用来指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来
    "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块默认导入
    "esModuleInterop": true, // 通过创建命名空间实现 CommonJS 兼容性
    "resolveJsonModule": true, // 自动解析JSON文件

    /* Source Map */
    "sourceRoot": "", // TypeScript 源代码所在的目录
    "mapRoot": "", // mapRoot用于指定调试器找到映射文件而非生成文件的位置,指定map文件的根路径,该选项会影响.map文件中的sources属性
    "inlineSourceMap": true, // 生成单个 sourcemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中

    /* 实验性 */
    "experimentalDecorators": true, // 启用实验性的装饰器特性
    "emitDecoratorMetadata": true // 为装饰器提供元数据支持
  },

    "files": [], // files可以配置一个数组列表,里面包含指定文件的相对或绝对路径,编译器在编译的时候只会编译包含在files中列出的文件,如果不指定,则取决于有没有设置include选项,如果没有include选项,则默认会编译根目录以及所有子目录中的文件。这里列出的路径必须是指定文件,而不是某个文件夹,而且不能使用* ? **/ 等通配符
    "include": [], // include也可以指定要编译的路径列表,但是和files的区别在于,这里的路径可以是文件夹,也可以是文件,可以使用相对和绝对路径,而且可以使用通配符,比如"./src"即表示要编译src文件夹下的所有文件以及子文件夹的文件
    "exclude": [], // exclude表示要排除的、不编译的文件,它也可以指定一个列表,规则和include一样,可以是文件或文件夹,可以是相对路径或绝对路径,可以使用通配符
    "extends": "", // extends可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置,继承来的文件的配置会覆盖当前文件定义的配置。TS在3.2版本开始,支持继承一个来自Node.js包的tsconfig.json配置文件
    "compileOnSave": true, // compileOnSave的值是true或false,如果设为true,在我们编辑了项目中的文件保存的时候,编辑器会根据tsconfig.json中的配置重新生成文件,不过这个要编辑器支持
    "references": [], // 一个对象数组,指定要引用的项目
}

常用选项解析

useDefineForClassFields

useDefineForClassFields 是 TypeScript 的一个编译选项,它用于控制类的实例属性如何编译。

这个选项的作用是:

  • 当它设置为true时,类的实例属性会编译成使用 Object.defineProperty 来定义。
  • 当它设置为false时(默认值),类的实例属性会编译成简单的赋值来定义。

举例说明:

class Foo {
  bar = 123; 
}

如果 useDefineForClassFields 为 true:


var Foo = class {
  constructor() {
    Object.defineProperty(this, "bar", {
      configurable: true,
      writable: true,
      value: 123
    });
  }
}

如果 useDefineForClassFields 为 false(默认):

var Foo = class {
  constructor() {
    this.bar = 123; 
  }
}

useDefineForClassFields 是一个用于类字段声明的配置选项。它在 TypeScript 2.7 版本中被引入,并在后续版本中得到改进和优化。

当开启 useDefineForClassFields 选项时,类字段的声明方式发生了改变,如下所示:

// 开启 useDefineForClassFields
class MyClass {
  myField = 42;
}

// 等价于未开启 useDefineForClassFields
class MyClass {
  constructor() {
    this.myField = 42;
  }
}

在未开启 useDefineForClassFields 选项的情况下,类字段的初始化通常需要放在构造函数中。而开启了该选项后,可以直接在类的定义中进行初始化,类似于在构造函数中使用赋值语句。

开启 useDefineForClassFields 的好处包括:

  1. 简洁性:代码更加简洁易读,不再需要在构造函数中手动初始化类字段,使得代码更加简洁。
  2. 可读性:通过直接在类定义中初始化字段,可以更清晰地看到字段的初始值,提高了代码的可读性。
  3. 类字段支持类型推断:开启该选项后,TypeScript 可以正确推断类字段的类型,不再需要显式地指定类型。

在 Vite v2.5.0 及更高版本中,如果 TypeScript 的 target 是 ESNext,则此选项的默认值为 true。这与 tsc v4.3.2 及更高版本的行为一致。这也是标准 ECMAScript 的运行时行为

isolatedModules

控制是否将每个文件作为单独的模块处理。

默认情况下,该选项为 true。这意味着:

  • 每个文件都作为单独的模块处理。
  • 文件之间的变量、函数、类等不会相互影响。
  • 文件只能访问自己导出的内容。

如果设置为 false:

  • 整个项目的文件会合并成一个模块。
  • 文件之间可以相互访问变量、函数、类等成员。
  • 一个文件可以直接使用另一个文件暴露出来的内容。

通常我们希望每个文件是独立的模块,所以保持该选项为 true。

但是有些场景下需要关闭该选项:

  • 当项目文件间有重名的类时,需要设置为 false 以便合并为一个类。
  • 当不同文件需要共享某些变量或状态时,需要设置为 false。

总之,isolatedModules 用于控制文件之间是否独立。它可以处理一些特殊的场景。

paths

paths 选项用于配置模块导入时的路径映射。通过使用 paths,你可以在项目中定义模块的别名,使得导入模块时可以使用这些别名,从而简化代码,减少路径错误,并提高可维护性。

以下是 paths 选项的使用示例:

  1. 假设你的项目目录结构如下:
project
|-- src
|   |-- components
|   |   |-- Button.ts
|   |   |-- Card.ts
|   |-- utils
|   |   |-- helpers.ts
|-- tsconfig.json
  1. 在 tsconfig.json 文件中,你可以这样配置 paths 选项:
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@components/*": ["components/*"],
      "@utils/*": ["utils/*"]
    }
  }
}

在这个例子中:

  • "baseUrl": "./src" 表示项目中所有导入模块的相对路径都是相对于 "src" 目录的。

  • "paths" 配置了两个路径映射:

    • "@components/*" 是一个模块别名,它将匹配任何以 "@components/" 开头的导入路径。
    • "@utils/*" 是另一个模块别名,它将匹配任何以 "@utils/" 开头的导入路径。
  1. 使用别名进行模块导入:

在你的代码中使用这些别名进行模块导入,而不需要使用相对路径或绝对路径。

// 使用别名导入模块
import { Button } from "@components/Button";
import { Card } from "@components/Card";
import { someHelperFunction } from "@utils/helpers";

// 使用别名进行导入后的代码更加简洁

通过使用 paths 选项,你可以定义更多的模块别名,根据你的项目结构和需求来简化导入语句,提高代码的可读性和可维护性

baseUrl

用于指定 TypeScript 编译器在解析模块导入路径时的基础路径。通过设置 baseUrl,可以简化模块导入语句,使得导入模块时的路径更加清晰和简洁。

假设项目结构如下:

project
|-- src
|   |-- components
|   |   |-- Button.ts
|   |   |-- Card.ts
|   |-- utils
|   |   |-- helpers.ts
|-- tsconfig.json

默认情况下,TypeScript 编译器会将所有导入语句视为相对于包含它们的文件的路径。这意味着,如果你在 Button.ts 中想导入 Card.ts,通常需要使用相对路径来完成导入,例如:

import { Card } from '../Card';

但是,随着项目规模增长,导入语句中的相对路径可能会变得非常复杂和深层嵌套,这可能导致代码可读性降低,维护困难。

为了解决这个问题,可以使用 baseUrl 选项,将模块导入路径的基础路径设置为项目中的某个目录,通常是项目的 src 目录。

在 tsconfig.json 文件中设置 baseUrl 的用法如下:

{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}

上面的配置将 baseUrl 设置为项目中的 src 目录。这意味着,在所有的模块导入语句中,TypeScript 编译器会将导入路径解析为相对于 ./src 目录的路径。因此,我们可以简化模块导入语句,如下所示:

// 使用 baseUrl 后,导入语句更简洁
import { Card } from 'components/Card';
import { someHelperFunction } from 'utils/helpers';

通过设置 baseUrl 选项,简化模块导入语句,提高代码的可读性和可维护性,尤其在大型项目中,这个功能尤为有用。

rootDirs

它用于指定 TypeScript 编译器应该搜索源文件的根目录。通常情况下,TypeScript 编译器会搜索项目中的所有源文件,并根据相对路径和引用关系进行编译,保留源文件结构。但有时,你可能希望将多个不同目录下的源文件编译到同一个输出目录中,这时就可以使用 rootDirs 选项。

示例:

假设你有一个项目,其中包含两个源文件目录:src1 和 src2,它们分别包含一些 TypeScript 文件。你希望将这两个目录中的文件编译到一个输出目录,比如 dist 中。

首先,你可以在 tsconfig.json 文件中配置 rootDirs 选项:

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDirs": ["./src1", "./src2"]
  }
}

在上面的示例中,我们指定了两个根目录:./src1 和 ./src2。TypeScript 编译器会在这两个目录中搜索源文件,并将它们编译到 ./dist 目录中。这样,无论源文件在哪个目录中,编译后的输出都会放在同一个输出目录中。

rootDir

rootDir 是用来指定 TypeScript 编译器应该从哪个根目录开始查找源代码文件。

通常情况下,你会将 rootDir 设置为项目的根目录,这样 TypeScript 编译器将会从这个目录开始递归地查找源文件。

当你只有一个根目录时,使用 rootDir 就足够了。

typeRoots

typeRoots 用于指定 TypeScript 类型声明文件(.d.ts 文件)的根目录。类型声明文件用于提供类型信息,以帮助 TypeScript 编译器理解第三方库或模块的类型。通过配置 typeRoots告诉 TypeScript 编译器在哪些目录中查找类型声明文件。

默认情况下,类型文件查找规则:

  • 在项目根目录下的 node_modules/@types 目录中查找与你的项目依赖库匹配的类型声明文件。这是因为许多第三方库的类型声明文件通常会安装在 @types 目录下。

  • 在项目根目录下的 node_modules 目录中查找与你的项目依赖库匹配的类型声明文件。

  • 在全局安装的 TypeScript 类型声明文件目录中查找,通常是 lib 目录。

  • 如果上述步骤都未找到匹配的类型声明文件,TypeScript 将会在相对于导入模块的文件夹中查找类型声明文件。这是一种后备机制,用于处理那些没有被正确配置的第三方库或自定义模块。

当你指定了typeRoots后,TypeScript编译器会从你指定的路径去引入声明文件,而不是node_modules/@types。

例如,如果你的配置如下:

{
  "compilerOptions": {
    "typeRoots": ["./typings", "./vendor/types"]
  }
}

那么编译器将会包含所有在./typings和./vendor/types下面的包,而不会包含任何在./node_modules/@types下面的包。所有的路径都是相对于tsconfig.json文件的。

如果你希望在自定义路径下没有找到匹配的声明文件时,编译器能够回退到默认的node_modules/@types路径,你可以在typeRoots中包含这个路径。例如:

{
  "compilerOptions": {
    "typeRoots": ["./typings", "./vendor/types", "./node_modules/@types"]
  }
}

这样,即使在./typings和./vendor/types中没有找到匹配的声明文件,编译器也会去./node_modules/@types中查找。

types

types 用于指定哪些类型声明文件应该被包含在编译过程中。通过配置 types,你可以选择性地包含或排除特定的类型声明文件,types支持配置模块名。

默认情况下,所有可见的@types包都会被包含在你的编译中。例如,./node_modules/@types/,../node_modules/@types/,../../node_modules/@types/等任何包含文件夹的node_modules/@types中的包都被视为可见。

如果指定了types,则只有列出的包才会被包含在全局范围内。例如:

{
  "compilerOptions": {
    "types": ["node", "jest", "express"]
  }
}

这个tsconfig.json文件只会包含./node_modules/@types/node, ./node_modules/@types/jest, 和 ./node_modules/@types/express. 其他在 node_modules/@types/* 下的包将不会被包含。

还可以自定义类型,这是你自定义的类型声明文件,它们位于项目中的某个位置,例如:

// my-custom-types.d.ts

declare module 'my-module' {
  // 在这里定义你的自定义类型或扩展已有的类型
  interface MyCustomType {
    // ...
  }

  // 如果需要,也可以导出变量、函数等
  const myVar: string;
  function myFunction(): void;
}

{
  "compilerOptions": {
    "types": ["my-module"]
  }
}

通过这样的配置,TypeScript 编译器将仅包含指定的类型声明文件,而忽略其他类型声明文件。这有助于减少不必要的类型信息加载,提高编译速度。如果不指定 types 选项,TypeScript 编译器将根据默认规则自动包含项目中的所有类型声明文件。

如果同时配置了 typeRoots 和 types,types 选项的设置将优先生效,即 TypeScript 编译器将根据typeRoots中包含的且 types 指定的文件来包含类型声明。

如果只配置了 typeRoots 而没有配置 types,那么 TypeScript 编译器将根据 typeRoots 中指定的目录来查找类型声明文件,但不会自动包含这些文件。你需要确保在代码中显式地导入需要的类型声明。

所以,你可以理解为 typeRoots 是用来限定类型声明的搜索文件范围的,types才是用来指定具体要包含的类型声明。

skipLibCheck

用于控制是否在编译时跳过对声明文件(.d.ts 文件)的类型检查。

在 TypeScript 项目中,除了普通的 TypeScript 文件外,我们还可能会引入第三方库或其他模块,这些模块通常会伴随着对应的声明文件,以便 TypeScript 在编译时能够正确地识别这些库的类型信息。有时候这些声明文件可能并不完善或存在一些错误,导致 TypeScript 编译器在检查时会发出一些类型相关的警告或错误。

开启 skipLibCheck 选项会提高编译速度但是可能会导致一些类型相关的问题在运行时才暴露出来,

moduleResolution

moduleResolution 选项用于配置模块解析策略,它有以下可选值:

  1. "node": 使用Node.js的模块解析策略。这是默认值。

    • 当遇到相对路径(如 ./module)时,TypeScript 会根据相对路径来查找文件。

    • 当遇到非相对路径(如 module)时,TypeScript 会从 node_modules 文件夹中查找对应的模块。如果找不到,会向上级目录查找,直到根目录。

    • 支持解析 .js, .json, .node 文件(以及使用 TypeScript 时的 .ts 文件)。

  2. "classic": 使用TypeScript早期的模块解析策略,已废弃。

  3. "bundler": 专为 bundler(如 Webpack、Rollup、Vite 等)设计的模块解析策略。

    • 会根据 package.json 中的 exportsimports 字段进行解析。
    • 如果项目依赖 Webpack、Rollup、Vite 等工具来打包前端代码,尤其是那些需要兼容各种模块格式和路径别名的复杂项目时选择
  4. "node16 / nodenext":适用于 Node.js 16 及更高版本的模块解析策略,nodenext 包含更进一步的支持

    • 支持 ESM(ECMAScript Modules)和 CommonJS 的混合模块解析。

    • 支持文件扩展名的显式解析(.js, .ts, .mjs, .mts, .cjs, .cts 等)。

    • 支持 package.json 中的 exportsimports 字段。

    • 如果你的项目主要运行在 Node.js 环境中,并且你需要支持或使用 ECMAScript Modules (ESM) 和 CommonJS (CJS) 两种模块格式,选择 node16nodenext 是更合适的。

    • nodenextnode16 的区别nodenextnode16 的增强版本,包含了更多对 ESM 和 TypeScript 特性的支持。如果你需要处理更复杂的 ESM 模块或者有较新的 TypeScript 特性需求,选择 nodenext

注意,需要解析 package.json 中的 exportsimports 字段配置的导出路径时,要选这两个。

当使用 NodeNext 模块解析策略时,编译器会采用与 ECMAScript 模块相似的解析策略,导入语句需要包含实际的文件扩展名(即编译后文件扩展名,例如不能使用.ts,必须使用.js)。

如果希望在导入中使用 .ts 扩展名,需要启用 allowImportingTsExtensions 选项,并同时设置了 noEmitemitDeclarationOnly, 否则如果生成了生成实际的 JavaScript 输出文件会无法解析ts,造成运行时解析问题。

如果项目使用打包工具,建议将 moduleResolution 设置为 bundler

allowImportingTsExtensions

allowImportingTsExtensions 是 TypeScript 4.7 引入的一个编译选项,允许你在导入 TypeScript 文件时包含 .ts.tsx 文件扩展名

主要作用:

  1. 允许导入带有扩展名的文件: 在设置 allowImportingTsExtensions: true 的情况下,像这样导入 TypeScript 文件:

    import { myFunction } from './myModule.ts';
    

    这种写法在某些场景下是必需的,比如当你使用一些工具或模块解析器时,会要求明确地指定扩展名。

  2. 支持 ESM 语法: 在使用 ES 模块(ESM)时,明确地指定文件扩展名更符合规范。

  3. 避免冲突: 如果有多个文件使用相同的名称,但扩展名不同(例如 myModule.tsmyModule.js),这个选项可以帮助明确指定要导入的文件,避免冲突或不必要的混淆。

resolveJsonModule

它用于控制 TypeScript 编译器是否支持导入 JSON 文件作为模块。

TypeScript 2.9 版本起,通过设置 resolveJsonModule 选项为 true,TypeScript 编译器会自动解析并识别导入的 JSON 文件,无需手动添加类型声明文件。

使用这个选项的好处在于,当你需要导入配置文件、静态数据或其他 JSON 文件时,可以直接在 TypeScript 文件中导入并使用它们,而无需手动处理类型声明。TypeScript 编译器会在编译过程中将 JSON 文件转换为一个对象,可以直接按照对象的属性来访问 JSON 文件的内容。

emitDeclarationOnly

TypeScript 编译器只会生成类型声明文件(.d.ts 文件),而不会生成编译后的 JavaScript 文件。这对于创建类型声明库非常有,并且能够提高构建速度。

需要注意的是,如果设置了 emitDeclarationOnly,必须将 declaration 设置为 true

noEmit

当设置 noEmit 选项为 true 时,TypeScript 编译器将不会生成任何输出文件,包括 JavaScript 文件和类型声明文件(.d.ts 文件)。

这个选项通常在以下情况下使用:

  1. 代码检查:你只想进行 TypeScript 代码的类型检查,而不需要实际编译生成 JavaScript 文件。通过设置 noEmit 可以加快检查速度,特别是在大型项目中。
  2. 在编辑器中进行类型检查:一些编辑器和 IDE(如 Visual Studio Code)支持在保存 TypeScript 文件时进行实时类型检查。如果你只想在编辑器中进行类型检查而不需要生成文件,可以设置 noEmittrue

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

jsx

它用于指定 TypeScript 文件中 JSX 语法的处理方式。

jsx 选项有以下几个可能的值:

  • "preserve": 这是默认值。当设置为 "preserve" 时,TypeScript 编译器会保留 JSX 语法不进行转换。当你使用非 React 的框架或工具处理 JSX 时(例如 Vue 或某些自定义的编译器),可以使用这个选项,当你有其他工具链负责处理 JSX(例如 Babel)时,也可以使用这个选项。。
  • "react": 当设置为 "react" 时,TypeScript 编译器会将 JSX 语法转换为 React.createElement 函数调用。这是在不使用 JSX 转换工具(如 Babel)的情况下,直接使用 TypeScript 编译器进行 JSX 转换的方式。这样你就可以在 TypeScript 项目中直接使用 JSX 语法,而不需要额外的转换配置。
export const helloWorld = () => <h1>Hello world</h1>;

转换为

import React from 'react';

export const helloWorld = () => React.createElement("h1", null, "Hello world");

  • "react-jsx": 从 TypeScript 4.1 开始支持的新 JSX 转换模式,使用的是新的 JSX 转换方式,不再依赖全局的 React 导入,适合 React 17 及以上版本,使用 React 17 引入的新 JSX 运行时,减少了对 React 的显式导入。
export const helloWorld = () => <h1>Hello world</h1>;

转换为
import { jsx as _jsx } from "react/jsx-runtime";

export const helloWorld = () => _jsx("h1", { children: "Hello world" });

  • "react-jsxdev": 类似于 "react-jsx",但包含了用于开发环境的调试信息。
  • "react-native": 在开发 React Native 应用时,通常会使用这个选项。React Native 使用 JSX,但它不会在编译阶段转换为 React.createElement,JSX 语法会被保留在编译后的 JavaScript 文件中,在运行时由 React Native 的解析器处理。

jsxImportSource

这个配置指定了在使用 "react-jsx""react-jsxdev" 模式时,JSX 表达式应该从哪里引入 jsxjsxs 函数。

  • 默认行为: 如果不设置这个选项,TypeScript 会假设你使用的是 React,因此它会从 react/jsx-runtime 引入

  • jsxImportSource: "preact" : 将 JSX 转换过程中的函数调用指向 Preact,而不是 React。

JSX runtime 是负责在运行时创建虚拟 DOM 元素的底层代码。

  1. JSX Runtime 的作用: JSX 代码最终需要被转换成实际的 JavaScript 代码执行。比如:

// JSX 代码
<div className="greeting">Hello</div>

// 转换后需要变成类似这样的函数调用
jsx('div', { className: 'greeting', children: 'Hello' })

这个转换过程涉及两个部分:

  • 编译时转换(由 Babel 或 TypeScript 完成)
  • 运行时支持(由 JSX runtime 提供)
  1. 新旧 Runtime 对比

旧版 Runtime (React 17 之前):


// 源码
function Greeting() {
  return <div>Hello</div>
}

// 转换后
import React from 'react';  // 需要显式导入 React
function Greeting() {
  return React.createElement('div', null, 'Hello');
}

新版 Runtime (React 17+): 新版运行时(React 17+):


// 源码
function Greeting() {
  return <div>Hello</div>
}

// 转换后
import {jsx as _jsx} from 'react/jsx-runtime'; // 自动导入
function Greeting() {
  return _jsx('div', { children: 'Hello' });
}
  1. 为什么需要新的 Runtime
  • 更好的性能(减少了一些运行时开销)
  • 更小的打包体积(不需要导入整个 React)
  • 更好的开发体验(不需要手动导入 React)
  • 为未来的优化留下空间
  1. 不同框架的 Runtime
  • React: react/jsx-runtime
    React: react/jsx-runtime
  • Preact: preact/jsx-runtime
    Preact: preact/jsx-runtime
  • Emotion: @emotion/react/jsx-runtime 等等...

这就是为什么需要 jsxImportSource 配置 - 它告诉编译器从哪个包来导入 JSX runtime。

jsxFactory

指定 JSX 编译时的工厂函数,也就是如何将 JSX 元素转换成函数调用。通常在非 React 项目(例如使用 Preact 或其他轻量库)中设置。

  • 作用

    • "jsxFactory": "h" 表示使用 h 函数来编译 JSX。
    • 例如 <div></div> 会被编译成 h("div", null)
    • 这个设置可以让 TypeScript 或 Babel 在编译时调用 h 函数,而不是默认的 React.createElement
  • 适用场景

    • 在 Preact 项目中,h 是 Preact 创建元素的函数,设置 "jsxFactory": "h" 可以让 JSX 直接调用 Preact 的 h 函数,而不是 React 的 createElement,从而减少包体积。
    • 使用其他自定义的轻量化框架或函数库,且该库提供了自己的 JSX 工厂函数(如 h)。

jsxFragmentFactory

指定了用于 JSX 片段(即 <></> 语法)的工厂函数。在 React 和 Preact 等库中,可以使用片段 Fragment 创建一个不包含额外 DOM 元素的包装层。

  • 作用

    • "jsxFragmentFactory": "Fragment" 表示使用 Fragment 函数来编译片段。
    • 例如 <></> 会被编译成 Fragment(null)h(Fragment, null),取决于 jsxFactory 的设置。
    • 在 React 中默认是 React.Fragment,而在 Preact 中可能是 Fragment
  • 适用场景

    • 在使用 Preact 或其他框架的项目中配置片段支持。
    • 使 JSX 片段语法 <></> 在项目中正确运行,同时减少不必要的 DOM 元素。

语法1:React.Fragment 标签


return (
  <React.Fragment>
    <ChildComponent1 />
    <ChildComponent2 />
  </React.Fragment>
);

语法2:简写<></>


return (
  <>
    <ChildComponent1 />
    <ChildComponent2 />
  </>
);
  • 不会产生额外的 DOM 元素:使用 React.Fragment,不会在 DOM 中增加额外的 <div> 或其他容器标签,避免了不必要的嵌套。

  • 提升性能:在渲染大量组件时,减少无意义的 DOM 层级可以稍微提升页面性能。

  • 兼容样式:避免嵌套的标签带来样式上的复杂性。

importHelpers

它用于控制 TypeScript 是否在编译时自动引入辅助工具函数,以减少生成的 JavaScript 代码大小。

在使用 TypeScript 编译时,一些高级的 JavaScript 特性(如 async/await、generator、Promise 等)需要一些辅助函数来实现,这些辅助函数会被频繁地使用。默认情况下,TypeScript 会将这些辅助函数内联到每个文件中,这可能导致生成的 JavaScript 文件比较冗长,特别是当项目中有多个文件使用这些特性时。

importHelpers 选项的作用是将这些辅助函数抽取到单独的帮助模块中,然后在每个文件中通过 import 语句引入这个帮助模块。这样可以避免生成冗长的重复代码,减小最终生成的 JavaScript 文件的大小,提高代码的运行效率。

当启用 importHelpers 时,TypeScript 会从 tslib 库中导入这些辅助函数,要在项目中安装该库。

如果你使用swc编译ts,并且开启了 importHelpers,则需要导入 @swc/helpers 包。

esModuleInterop

在 ES 模块系统中,导入和导出模块的语法是使用 importexport 关键字。而在 CommonJS 模块系统中,Node.js 早期使用的模块系统,导入模块的语法是使用 require,导出模块的语法是使用 module.exports

esModuleInterop 设置为 true 时,TypeScript 编译器会在生成的 JavaScript 代码中使用 ESM 格式来处理模块的导入和导出。同时,它还允许你在 TypeScript 代码中使用 import 来导入 CommonJS 格式的模块。

设置 esModuleInteroptrue 的好处是可以简化代码并提高互操作性。通过在 TypeScript 代码中使用 import 来导入 CommonJS 格式的模块,你可以统一使用一种导入导出的语法风格,避免混用 importrequire

allowSyntheticDefaultImports

用于允许从没有默认导出的模块中默认导入。

正常情况下,只有当一个模块明确导出一个默认值时,才可以使用默认导入语法导入该模块:

// module.ts
export default function foo() {}

// other.ts
import func from './module';

但是有些模块没有默认导出值,这时默认导入语法将不被允许:

// module.ts
export function foo() {}

// other.ts
import func from './module'; // Error

allowSyntheticDefaultImports 选项允许使用默认导入语法,即使这个模块没有默认导出值:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true
  }
}

// other.ts
import func from './module'; // Allowed

这在一些特殊场景下是有用的,例如允许默认导入 CommonJS 模块。

// CommonJS module
module.exports = {
  foo() {
    // ...
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true
  }
}

// TypeScript
import module from './module.js';

module.foo();

在这个例子中:

  1. module.js是一个 CommonJS 模块,使用module.exports导出对象而不是默认导出。
  2. 在tsconfig.json中开启allowSyntheticDefaultImports选项。
  3. 在TypeScript中可以直接默认导入这个模块,并像默认导出一样使用它。
  4. 这样使得导入 CommonJS 模块的语法与导入ES6模块的语法保持一致。
  5. TypeScript通过创建一个合成的默认导出来实现这一转换。

所以通过配置allowSyntheticDefaultImports,可以实现在TypeScript中将CommonJS模块平滑地默认导入的效果。

但是非必要场景下不建议开启此选项。因为这会破坏默认导入语法的语义,使得无法明确知道一个模块是否有默认导出,增加理解和维护的难度。

importHelpers

importHelpers 的作用是控制 TypeScript 是否在生成的 JavaScript 代码中引入辅助函数。

辅助函数是一些用来处理特定编译目标(如 ES5、ES6 等)的函数,它们包含在 TypeScript 核心库中,用于支持某些语言特性或编译后的代码结构。在某些情况下,引入这些辅助函数可能会导致生成的 JavaScript 代码更大,但在其他情况下,可以提供更好的兼容性和性能。

以下是 importHelpers 的两种配置方式的示例说明:

importHelpers: true(默认值):

默认情况下,TypeScript 会将辅助函数内联到每个编译后的文件中。这意味着每个生成的 JavaScript 文件都包含了这些辅助函数的副本。这样生成的代码可能会更大,但不需要额外的模块加载。

importHelpers: false:

如果将 importHelpers 设置为 false,TypeScript 将不再内联辅助函数,而是假定它们在运行时会由某个共享的模块提供。这可以减小生成的 JavaScript 文件的大小,但需要确保在运行时包含了 TypeScript 的辅助函数模块如 "tslib"。

一般来说,如果你的目标是生成较小的 JavaScript 文件并且你可以控制将 TypeScript 的辅助函数模块包含在项目中,那么将 importHelpers 设置为 false 可能是一个好选择。如果你不想关心这些辅助函数模块,或者希望在单个文件中包含所有必要的功能,那么可以保持默认值 true。

downlevelIteration

downlevelIteration 的作用是控制 TypeScript 在编译时如何处理迭代器(Iterators)和生成器(Generators)。这个选项通常用于处理与迭代相关的代码,特别是当目标编译版本为 ES5 或更早的 JavaScript 版本时。

downlevelIteration: true(默认值):

当将 downlevelIteration 设置为 true 时,TypeScript 将使用一种转换技术来生成代码,以便在目标 JavaScript 版本中模拟支持迭代器和生成器。这样可以确保在较旧的 JavaScript 运行时中运行与迭代相关的代码,但可能会导致生成的代码变得更复杂和冗长。

downlevelIteration: false:

如果将 downlevelIteration 设置为 false,TypeScript 将不会进行迭代器和生成器的降级处理。这意味着生成的代码将依赖于运行时环境对迭代器和生成器的支持。如果你的目标运行时环境已经支持迭代器和生成器,设置为 false 可以生成更简洁的代码。

通常情况下,保持默认配置就可以了,如果你的目标是在较旧的 JavaScript 环境中运行你的代码,将 downlevelIteration 设置为 true 以确保代码的可用性和兼容性。而ES6 和更高版本的 JavaScript 已经支持原生的迭代器和生成器,不需要额外的转换或降级处理即使配置为true也不会生成兼容代码。

moduleDetection

在 TypeScript 中,如果一个文件没有使用 importexport 语句,它通常会被视为全局脚本。这意味着文件中的所有变量、函数等都会默认被放置在全局作用域中。

moduleDetection: "force" 强制 TypeScript 将所有文件都视为模块,即使没有显式的 importexport 语句。这样做的好处是:

  1. 模块作用域:每个文件都被封装在自己的模块作用域内,防止意外的全局变量声明。
  2. 避免冲突:即使你忘记了使用 importexport,TypeScript 仍然会确保这个文件不会影响或被影响其他文件中的全局变量,减少命名冲突的风险。

举个例子:

在没有使用模块系统(即没有 importexport)的情况下,JavaScript 文件会被视为一个全局脚本。

fileA.ts

var message = "Hello from File A";
console.log(message);

fileB.ts

var message = "Hello from File B";
console.log(message);

如果这两个文件在同一 HTML 页面中被加载或在同一脚本中被执行,由于它们都声明了同名的变量 message,第二个文件的变量声明会覆盖第一个文件的变量声明。此时,两个文件的 message 变量都在全局命名空间中,造成了命名冲突和全局污染。

使用模块如何避免全局污染

在使用模块系统时,每个文件都会被视为一个独立的模块,拥有自己的作用域。以下是使用模块的同一示例:

fileA.ts

export const message = "Hello from File A";
console.log(message);

fileB.ts

export const message = "Hello from File B";
console.log(message);

在这种情况下,由于每个文件都有自己的模块作用域,message 变量不会相互冲突。即使两个文件声明了同名的变量,它们仍然保持在各自模块的作用域内,从而避免了全局污染。

incremental

启用增量编译,使用该标志可以加快后续构建速度,增量编译通过保存上一次编译的结果来避免重新编译未更改的部分,从而减少每次构建所需的时间。

启用该选项会自动生成编译信息文件 .tsbuildinfotsc将尝试使用该文件来增量类型检查并更新我们的输出文件。

tsc -b 启用增量编译模式。

composite

composite选项用于指定一个项目是否是一个“复合”项目,即它是否可以被其他项目引用。如果你设置了"composite": true,那么TypeScript编译器将生成额外的元数据,以便其他项目可以更快地构建这个项目。

当此设置开启时:

  • 如果未显式设置, rootDir设置默认为包含tsconfig.json文件的目录。
  • 所有实现文件必须通过include模式匹配或在files数组中列出。如果违反此约束, tsc将通知哪些文件未指定。
  • declaration默认为true

复合项目(将composite设置为true tsconfig.json )的部分目的是可以增量构建不同项目(子项目)之间的引用。因此,复合项目将始终生成.tsbuildinfo文件,即使你的项目没有被其他项目引用(也就是说,你没有使用references选项),composite选项仍然可以帮助提高构建性能。

references

references 是 TypeScript 3.0 中的一项新功能,允许将 TypeScript 程序分成多个项目,通过这样做,可以大大缩短构建时间,强制组件之间的逻辑分离,以更好的方式组织代码。

references选项用于指定一个项目依赖哪些其他项目,每个引用的path属性可以指向包含tsconfig.json文件的目录,或配置文件本身(可以有任何名称)。

当引用一个项目时:

  • 从引用的项目导入模块将改为加载其输出声明文件( .d.ts )

    例子:

    如果项目 A 引用项目 B,项目 A 导入项目 B 的模块时,会使用项目 B 的 .d.ts 文件中的类型信息,而不是项目 B 的源代码。

  • 如果项目 B 的 tsconfig.json 配置了 outFile 选项,那么编译后的 .d.ts 文件将包含所有模块的声明,并且这些声明将会被项目 A 看到和使用。

  • tsc --build 将自动构建引用的项目

tsc --build 是 TypeScript 编译器的一个命令,用于编译项目并启用增量编译功能,运行该命令后将执行

  • 查找所有引用的项目
  • 检测它们是否是最新的
  • 以正确的顺序构建过时的项目

假设你有一个大型的TypeScript项目,它由多个子项目组成,每个子项目都有自己的源代码和tsconfig.json文件。例如:

my-large-project/
├── tsconfig.json
├── subproject1/
│   ├── tsconfig.json
│   └── src/
│       └── index.ts
└── subproject2/
    ├── tsconfig.json
    └── src/
        └── index.ts

subproject1和subproject2都是独立的子项目,它们各自有自己的源代码和配置。

你可以在每个子项目的tsconfig.json文件中设置"composite": true,这样TypeScript编译器就知道这些项目是可以单独编译的。例如,subproject1/tsconfig.json可能如下:

{
  "compilerOptions": {
    "composite": true,
    // 其他编译器选项...
  },
  // 文件和包含模式...
}

然后,在主项目的tsconfig.json文件中,可以使用references选项来引用这些子项目:

{
  "compilerOptions": {
    // 编译器选项...
  },
  "references": [
    { "path": "./subproject1" },
    { "path": "./subproject2" }
  ]
}

当在主项目目录下运行tsc --build命令时,TypeScript编译器会自动按照正确的顺序构建所有子项目。如果subproject2依赖于subproject1,那么TypeScript会首先构建subproject1,然后再构建subproject2。

这种方式可以更好地组织和构建大型TypeScript项目,并且可以提高构建性能,因为TypeScript编译器可以跳过已经构建过的子项目。

如果你有一个小型项目,没有复杂的依赖关系,并且不需要特别关注编译性能,那么可以不使用这两个选项。

如果你的项目很大,包含多个源文件,并且你希望通过指定依赖关系来管理它们的编译顺序,可以配置 references和composite,当拆分成多个工程后,会显著地加速类型检查和编译,减少编辑器的内存占用。