跳到主要内容
预计阅读 39 分钟

新人接手项目两眼一黑——tsconfig与工具链的系统化配置

一次痛苦的新人入职

某团队新来的工程师在本地 npm install 之后执行 npm run build,控制台刷出了 300 多个类型错误。老成员的解释是”那些报错不用管,CI 上会过的”。深入排查后发现:本地 Node.js 版本与 CI 不一致导致 @types/node 版本冲突;tsconfig.json 里的 strict 被设为 false,大量隐式 any 在暗处腐蚀代码质量;ESLint 规则和 Prettier 格式化互相冲突,保存文件后自动格式化反而引入了 lint 错误。

这些问题的根源在于项目工程化配置缺乏系统性规划。一个配置混乱的项目就像一栋没有图纸的建筑——每个人都在按自己的理解添砖加瓦,结果就是越建越歪。

本章将从 tsconfig.json 的每一个关键配置项讲起,覆盖 ESLint/Prettier 集成、构建工具对接和 Monorepo 多包管理,建立一套可复制的工程化标准。读完本章后,你应该能够为任何新项目在十分钟内搭建出一套规范的工程化配置,让团队成员在统一的规则下协作。


tsconfig.json 全面拆解

它是什么,如何生成

tsconfig.json 是 TypeScript 编译器(tsc)的配置文件。当项目根目录存在此文件时,tsc 将该目录视为项目根并按文件中的配置编译。

快速生成一份带注释的模板:

tsc --init

生成的文件包含了所有可用选项的注释说明,其中大部分被注释掉了,只有几个最基础的选项被默认启用。对于新手来说,这份文件既是配置模板也是参考手册——遇到不熟悉的选项时,打开它搜索一下就能看到官方的解释。但不建议直接使用这份生成的配置——虽然 strict 已默认启用,但许多实用选项(如路径别名、增量编译等)仍需手动配置,不适合直接用于现代项目。

顶层属性一览

{
  "compilerOptions": {},
  "include": [],
  "exclude": [],
  "files": [],
  "extends": "",
  "references": []
}

files —— 精确列出要编译的文件,不支持通配符。适合文件数极少且固定的微型项目:

{ "files": ["src/entry.ts", "src/bootstrap.ts"] }

include / exclude —— 用通配符划定编译范围。三种通配符:?(单字符)、*(任意字符,不跨目录)、**(任意层级):

{
  "include": ["src/**/*", "typings/**/*"],
  "exclude": ["**/*.test.ts", "**/*.spec.ts", "node_modules"]
}

默认编译 .ts.tsx.d.ts;开启 allowJs 后包含 .js.jsx

extends —— 继承另一份配置文件,子文件的选项覆盖父文件同名项。可以继承本地文件或 npm 包:

{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "outDir": "./build"
  }
}

社区维护了 @tsconfig/recommended@tsconfig/node22@tsconfig/react-native 等预设包,可直接继承(选择与你的 Node.js LTS 版本对应的预设)。这些预设包经过社区大量项目验证,是新项目的优质起点。在继承的基础上只需要覆盖少数几个与项目特性相关的选项(如输出目录、路径别名等),就能获得一份经过验证的配置。

references —— 项目引用,用于 Monorepo 中将大项目拆分为多个独立编译单元,后文详述。


compilerOptions 核心选项分组详解

编译目标

target —— 输出的 JavaScript 版本。决定哪些语法特性会被降级转换:

{ "compilerOptions": { "target": "ES2022" } }

设为 ES5 时箭头函数会被转为 function,可选链会被转为三元表达式。常用值:ES2020ES2022ESNext。选择依据是你的目标运行环境——如果后端项目部署在 Node.js 18+,可以放心使用 ES2022;如果前端需要兼容较旧的浏览器,可能需要降低到 ES2015ES2017,但通常更好的做法是将降级工作交给 Babel 或打包工具处理,让 TypeScript 输出高版本 JavaScript。

lib —— 加载的内置类型声明库。不同运行环境需要不同的 lib:

{
  "compilerOptions": {
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  }
}

浏览器项目需要 DOM;纯 Node.js 项目不需要。如果不指定,TypeScript 会根据 target 自动选择。

模块系统

module —— 输出的模块格式:

适用场景
CommonJSNode.js 传统项目
ESNext / ES2022前端打包器项目
Node16 / NodeNextNode.js 原生 ESM

moduleResolution —— 模块路径解析算法:

说明
node经典 CommonJS 解析
node16 / nodenext支持 ESM 的 Node.js 解析
bundlerTypeScript 5.0+ 新增,适配 Vite/webpack/esbuild

esModuleInterop —— 修复 ES Module 与 CommonJS 互操作问题。开启后可以用 import http from 'http' 代替 import * as http from 'http'建议始终开启。这个选项解决的是一个历史遗留问题:CommonJS 模块没有”默认导出”的概念,但 ES Module 有。不开启这个选项时,用 import 语法导入 CommonJS 模块会遇到各种诡异问题,比如 import express from 'express' 得到的是 undefined。开启后 TypeScript 会自动生成兼容代码来桥接两种模块系统。

路径映射

baseUrl + paths 配合实现路径别名:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@core/*": ["core/*"],
      "@infra/*": ["infrastructure/*"],
      "@shared": ["shared/index"]
    }
  }
}

使用:

import { createLogger } from "@core/logging";
import { PostgresAdapter } from "@infra/database";

重要提醒:paths 只作用于 TypeScript 编译时的路径解析。运行时需要在打包工具中做对应配置(Vite 的 resolve.alias、webpack 的 resolve.alias),否则运行时会报 “module not found”。

严格类型检查

strict 是一个总开关,启用后等价于同时开启以下全部子选项:

子选项效果
noImplicitAny禁止变量隐式推断为 any
strictNullChecksnull/undefined 不能赋给其他类型
strictFunctionTypes函数参数类型逆变检查
strictBindCallApply严格检查 bind/call/apply 参数
strictPropertyInitialization类属性必须在声明或构造函数中初始化
noImplicitThis禁止 this 隐式推断为 any
useUnknownInCatchVariablescatch 变量类型为 unknown 而非 any
alwaysStrict输出文件包含 "use strict"

新项目必须开启 strict: true。对于从 JavaScript 迁移的遗留项目,可以先关闭再逐项开启。

一个常见的疑问是:开启严格模式后报了几百个错误怎么办?正确的策略不是关掉 strict,而是分阶段推进。先把 strict 关掉,逐个开启子选项。优先开启影响最大的 strictNullChecks——它能发现最多的潜在空指针错误,是 TypeScript 类型安全最核心的保障。然后依次开启 noImplicitAnystrictFunctionTypes 等,每开一个就修一批错误。这个过程虽然痛苦,但每修复一个错误,都在减少一个潜在的线上 Bug。

输出控制

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationDir": "./dist/types",
    "sourceMap": true,
    "removeComments": true,
    "noEmit": false,
    "emitDeclarationOnly": false
  }
}
选项说明
outDir编译产物的输出目录
rootDir源码根目录,影响输出的目录结构
declaration生成 .d.ts 类型声明文件
sourceMap生成 .map 文件用于调试
noEmit仅做类型检查,不生成任何文件
emitDeclarationOnly只生成 .d.ts,不生成 .js

其他实用选项

{
  "compilerOptions": {
    "resolveJsonModule": true,
    "allowJs": true,
    "checkJs": false,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "isolatedModules": true,
    "jsx": "react-jsx"
  }
}
  • resolveJsonModule:允许直接 import JSON 文件
  • skipLibCheck:跳过 .d.ts 的类型检查,大幅加速编译
  • forceConsistentCasingInFileNames:强制文件名大小写一致,避免跨平台问题
  • incremental:增量编译,生成 .tsbuildinfo 缓存
  • isolatedModules:确保每个文件可独立编译,适配 esbuild/Babel 等单文件编译器
  • jsx:JSX 处理模式,React 17+ 推荐 react-jsx

两套推荐模板

前端项目(React + Vite)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "baseUrl": "./src",
    "paths": { "@/*": ["./*"] }
  },
  "include": ["src"]
}

Node.js 后端服务

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "resolveJsonModule": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

tsc 命令行速查

# 按 tsconfig.json 编译
tsc

# 编译单个文件(忽略 tsconfig.json)
tsc src/main.ts

# 指定配置文件
tsc -p tsconfig.production.json

# 监听模式
tsc -w

# 仅类型检查,不生成文件
tsc --noEmit

# 项目引用的增量构建
tsc -b

# 清除增量编译缓存
tsc -b --clean

命令行参数优先级高于 tsconfig.json 中的同名配置。

在 CI/CD 流水线中,最常用的命令是 tsc --noEmit——它只做类型检查不生成文件,适合作为代码合入主分支前的质量关卡。如果类型检查不通过,流水线直接失败,避免带着类型错误的代码进入主分支。对于大型项目,可以结合 --incremental 选项加速 CI 中的类型检查,它会利用上一次检查的缓存来跳过没有变化的文件。


TypeScript 注释指令

在代码中局部控制类型检查行为的四个特殊注释:

在日常开发中,你偶尔会遇到 TypeScript 报错但你确信代码是正确的情况——可能是第三方库的类型定义有误,也可能是 TypeScript 的类型推断在极端场景下不够聪明。这时候,注释指令就是你的应急工具。但请注意:它们是”灭火器”而非”日常工具”,过度使用等于放弃了类型安全的保护。

@ts-ignore —— 忽略下一行的类型错误(慎用):

let port: number;
// @ts-ignore
port = process.env.PORT; // 不报错,但运行时可能出问题

@ts-expect-error —— 同样忽略下一行错误,但如果下一行实际没有错误会产生警告。适合测试代码中刻意传入错误类型的场景:

function multiply(a: number, b: number) { return a * b; }
// @ts-expect-error 故意传错类型以测试运行时行为
multiply("two", 3);

@ts-nocheck —— 放在文件开头,跳过整个文件的类型检查。

@ts-check —— 放在 .js 文件开头,为该 JS 文件启用类型检查:

// @ts-check
let counter = 0;
counter = "reset"; // 在 JS 文件中也会报类型错误

ESLint 与 TypeScript 的协作

职责划分

TypeScript 编译器负责类型正确性;ESLint 负责代码风格最佳实践。两者互补,不可替代。一个常见的误解是”有了 TypeScript 就不需要 ESLint 了”。实际上,TypeScript 只关心类型是否正确,它不会告诉你”这里应该用 const 而不是 let”,也不会提醒你”这个 Promise 的返回值没有被 await”。这些代码质量问题正是 ESLint 的守备范围。特别是 @typescript-eslint 提供的类型感知规则,能够利用 TypeScript 的类型信息发现更深层次的代码问题,比如不安全的类型断言、多余的类型注解等。

安装

npm install --save-dev eslint typescript \
  @typescript-eslint/parser \
  @typescript-eslint/eslint-plugin

传统配置格式(.eslintrc.js)——已弃用

注意:ESLint 9(2024 年 4 月发布)已正式弃用 .eslintrc.* 配置格式,新项目应直接使用下方的 Flat Config 格式。此处保留传统格式仅供维护旧项目时参考。

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  parserOptions: {
    project: "./tsconfig.json",
    ecmaVersion: 2022,
    sourceType: "module",
  },
  plugins: ["@typescript-eslint"],
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-type-checked",
  ],
  rules: {
    "no-var": "error",
    "@typescript-eslint/consistent-type-definitions": ["error", "interface"],
    "@typescript-eslint/no-unused-vars": [
      "error",
      { argsIgnorePattern: "^_" },
    ],
    "@typescript-eslint/explicit-function-return-type": "warn",
  },
};

Flat Config 新格式(ESLint 9+)

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_" },
      ],
    },
  }
);

运行

ESLint 8 和 ESLint 9 的命令行用法有差异。ESLint 9 的 Flat Config 通过配置文件本身控制文件匹配,不再需要 --ext 标志:

// ESLint 9+(Flat Config)
{
  "scripts": {
    "lint": "eslint src",
    "lint:fix": "eslint src --fix"
  }
}
// ESLint 8(旧版 .eslintrc 格式)
{
  "scripts": {
    "lint": "eslint src --ext .ts,.tsx",
    "lint:fix": "eslint src --ext .ts,.tsx --fix"
  }
}

Prettier 集成

解决与 ESLint 的规则冲突

Prettier 专注代码格式化,与 ESLint 的格式相关规则可能冲突。推荐方案是用 eslint-config-prettier 关闭冲突规则,然后独立运行 Prettier:

npm install --save-dev prettier eslint-config-prettier
  • eslint-config-prettier:关闭 ESLint 中所有与 Prettier 冲突的规则

社区早期常用 eslint-plugin-prettier 将 Prettier 作为 ESLint 规则运行,但 Prettier 官方现已不再推荐这种方式——它会拖慢 lint 速度且产生不必要的噪音。更好的做法是让 ESLint 和 Prettier 各司其职,分别独立运行。

如果你使用 ESLint 9+ 的 Flat Config(推荐),在配置末尾追加 eslint-config-prettier。注意这里统一使用 tseslint.config() 包裹(与前文 ESLint 配置示例保持一致):

// eslint.config.js
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  eslintConfigPrettier, // 必须放最后,关闭所有与 Prettier 冲突的规则
);

如果你仍在使用旧版 .eslintrc 格式(ESLint 8 及以下),则在 extends 末尾追加 "prettier"

module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier", // 必须放最后
  ],
  plugins: ["@typescript-eslint"],
};

然后在 package.json 中分别配置 lint 和格式化命令:

{
  "scripts": {
    "lint": "eslint src",
    "format": "prettier --write src"
  }
}

Prettier 配置文件

// prettier.config.js
module.exports = {
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: true,
  trailingComma: "es5",
  bracketSpacing: true,
  arrowParens: "always",
};

与构建工具的集成

Vite + TypeScript

Vite 内置 TypeScript 支持,使用 esbuild 做转译——速度极快但不做类型检查

npm create vite@latest my-app -- --template react-ts

Vite 之所以不做类型检查有两个原因:第一,类型检查需要全局分析所有文件的类型关系,这与 Vite 按需编译单个文件的架构不兼容;第二,类型检查相对耗时,如果在每次文件变更时都做完整检查,会严重拖慢开发服务器的响应速度。因此 Vite 的策略是”先跑起来再说”,类型错误不阻塞页面渲染。

路径别名需要在 vite.config.ts 中同步配置:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

开发时并行类型检查可以用 vite-plugin-checker

import checker from "vite-plugin-checker";

export default defineConfig({
  plugins: [
    react(),
    checker({ typescript: true }),
  ],
});

webpack + TypeScript

两种方案:

ts-loader(自带类型检查,速度较慢):

module.exports = {
  entry: "./src/main.ts",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
};

esbuild-loader(更快,无类型检查):

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "esbuild-loader",
        options: { target: "es2022" },
      },
    ],
  },
};

esbuild 直接使用

esbuild 原生支持 TypeScript,但不做类型检查:

esbuild src/main.ts --bundle --outfile=dist/app.js --platform=node

工程实践中的标准搭配:

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc --noEmit && esbuild src/main.ts --bundle --outdir=dist"
  }
}

核心模式:esbuild/Vite 负责快速编译,tsc 负责类型检查,各司其职。

为什么要分两步?因为 TypeScript 的类型检查是一个全局分析过程——编译器需要同时看到所有文件才能正确推断类型关系,这决定了它无法被并行化。而 esbuild 是逐文件编译,每个文件独立处理,可以充分利用多核 CPU,速度能达到 tsc 的数十倍。将类型检查和代码转译拆开后,开发者在写代码时可以享受 esbuild 带来的亚秒级热更新,同时在后台或提交前运行 tsc 做完整的类型检查,两全其美。


Monorepo 中的 TypeScript 配置

项目引用(Project References)

大型项目常拆分为多个包(pnpm workspaces、Turborepo 等)。TypeScript 的 Project References 支持按依赖关系增量编译各子包。

目录布局

platform/
  tsconfig.json            -- 根 solution 文件
  tsconfig.base.json       -- 共享基础配置
  packages/
    common/
      src/
      tsconfig.json
      package.json
    api-gateway/
      src/
      tsconfig.json
      package.json
    web-console/
      src/
      tsconfig.json
      package.json

基础配置(所有子包继承)

// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "composite": true
  }
}

composite: true 是启用项目引用的前提条件。开启后 TypeScript 会要求每个子项目的输入文件必须被 includefiles 明确指定,并且每个子项目必须开启 declaration 以输出 .d.ts 文件——因为其他子项目需要通过这些声明文件来获取类型信息,而不是直接读取源码。这种设计确保了每个子包的编译是独立的,可以被单独缓存和增量更新。

根 solution 文件

// tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./packages/common" },
    { "path": "./packages/api-gateway" },
    { "path": "./packages/web-console" }
  ]
}

"files": [] 表示根配置本身不编译任何文件,只负责组织子项目。

子包配置

// packages/api-gateway/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../common" }
  ]
}

references 声明了该子包依赖 common 包,tsc -b 会自动按正确顺序构建。

构建命令

# 增量构建整个项目,自动解析依赖顺序
tsc -b

# 清除编译缓存
tsc -b --clean

# 强制全量重新编译
tsc -b --force

本章回顾

本章从”新人接手项目寸步难行”的场景出发,系统梳理了 TypeScript 工程化的完整配置体系:

  • tsconfig.json 的顶层属性(include/exclude/extends/references)和 compilerOptions 的分组详解
  • strict 模式是类型安全的底线,新项目必须开启
  • ESLint 负责代码风格和最佳实践,通过 @typescript-eslint 系列包与 TypeScript 深度集成
  • Prettier 负责格式化,通过 eslint-config-prettier 消除与 ESLint 的冲突
  • 构建工具集成的核心原则:esbuild/Vite 做快速转译,tsc 做类型检查,分工协作
  • Monorepo 配置通过 Project References 和 composite 选项实现按依赖关系的增量编译

工程化配置不是一劳永逸的事情。随着 TypeScript 版本升级、构建工具迭代和团队规模变化,配置也需要定期审视和调整。建议每次 TypeScript 大版本升级时(如从 5.x 到 6.x),都重新审视 tsconfig.json,了解新版本增加了哪些有价值的选项,以及哪些旧选项已被标记为废弃。保持配置与时俱进,是团队工程效率持续改进的基础。

购买课程解锁全部内容

告别类型错误:12 章掌握 TypeScript 工程实战

¥29.90