首页 / TypeScript 入门教程 / tsconfig.json 详解(下)

TypeScript 入门教程

tsconfig.json 详解(下)

本教程共 80 篇 · 第 6 篇 · 更新于 2026-08-10 · 约 18 分钟阅读

TypeScriptTypeScript 入门教程tsconfigstrictmoduletarget编译选项

本节目标:掌握 compilerOptions 里最常用的 9 类选项。学完你会配置 strict 类型检查、选择合适的 module 和 target、用 paths 设路径别名、控制输出目录和声明文件——日常开发中 90% 的 tsconfig 配置你都能自己搞定。

上一章画了 compilerOptions 的全景地图。这一章拿着放大镜,逐个看那些你每天都会打交道的选项。每个选项讲清楚三件事:它是干什么的、什么时候该开、TypeScript 7.0 改了啥。

strict 家族:8 项检查,1 个总开关

strict 是 compilerOptions 里最重要的一个选项。把它设成 true,相当于同时打开了 8 个子检查。TypeScript 7.0 默认 strict: true,新建项目什么都不写就已经是最严格模式。

{
  "compilerOptions": {
    "strict": true
  }
}

这 8 个子项是:

子选项检查什么
alwaysStrict编译产出的 JS 文件自动加 "use strict"
strictNullChecksnullundefined 不能随便赋值给其他类型
strictBindCallApplybind / call / apply 的参数类型要匹配
strictFunctionTypes函数参数类型检查更严格(逆变检查)
strictPropertyInitialization类的属性必须在构造函数里初始化
noImplicitAny不允许隐式推断为 any
noImplicitThis不允许隐式 this: any
useUnknownInCatchVariablescatch 的变量默认是 unknown 而不是 any

下面拆开看其中几个最容易踩坑的。

strictNullChecks:null 和 undefined 分家

不开 strictNullChecks 时,nullundefined 可以赋值给任意类型:

let name: string = null;  // 不报错——但这是定时炸弹

开了之后,TypeScript 会把 nullundefined 当作独立类型来处理:

let name: string = null;
//  ❌ Type 'null' is not assignable to type 'string'.

let name: string | null = null;  // ✅ 明确声明可能为 null

这个选项是 TypeScript 类型安全的核心。不开它,类型系统等于半废。

noImplicitAny:不准偷懒用 any

当一个变量、参数或返回值没法推断类型时,TypeScript 会默认给它 any 类型。这在不知不觉中制造了一堆类型盲区:

function greet(name) {
  // ❌ Parameter 'name' implicitly has an 'any' type.
  return `Hello, ${name}`;
}

开了 noImplicitAny,你必须显式标注类型:

function greet(name: string) {
  return `Hello, ${name}`;
}

noImplicitReturns:函数必须有明确的返回值

一个函数如果有的分支 return 了、有的没 return,这在运行时可能产生 undefined

function getStatus(code: number) {
  if (code === 200) {
    return "OK";
  }
  // ❌ Not all code paths return a value.
}

开了 noImplicitReturns,要么每个分支都 return,要么统一声明返回类型包含 undefined

function getStatus(code: number): string | undefined {
  if (code === 200) return "OK";
}

noUnusedLocals / noUnusedParameters:扫垃圾

这两个选项属于代码洁净度检查,不被 strict 收录但同样重要:

  • noUnusedLocals:声明了但没用过的局部变量报错
  • noUnusedParameters:声明了但没用过的函数参数报错
function calculate(a: number, b: number) {
  // ❌ 'b' is declared but never read.
  return a * 2;
}

这在多人协作项目里很有用——提醒你及时清理死代码。

Tip

strict 默认不包含 noUnusedLocalsnoUnusedParameters。如果你想让这两个也生效,需要在 tsconfig 里显式开启。

module:编译产物用什么模块系统

module 决定 tsc 输出的 JavaScript 使用哪种模块语法。TypeScript 7.0 默认值是 esnext

可选值

产物语法适用场景
esnextimport / export现代打包工具(Vite、Webpack、esbuild)
commonjsrequire / module.exportsNode.js 传统项目
node16 / nodenext.mts/.cts 扩展名自动选择Node.js 12+ 原生 ESM
preserve原样保留 import / export由其他工具处理模块转换

7.0 变更

TypeScript 7.0 重要变更AMDUMDSystemJS 已被移除。如果你的旧项目 tsconfig 里写的是这三个值之一,迁移到 7.0 后会直接报错。请改为 esnextcommonjs

大多数新项目直接用默认的 esnext 就好。前端打包工具能处理 ESM 语法,Node.js 22+ 也原生支持 ESM。

moduleResolution:模块怎么找到文件

moduleResolution 控制 TypeScript 如何根据 import 路径找到对应的文件。7.0 支持三种策略:

行为推荐场景
bundler模拟打包工具(Vite/Webpack)的解析逻辑,支持无扩展名导入前端项目
node16 / nodenext模拟 Node.js 的 ESM/CJS 解析,文件扩展名必须写 .jsNode.js 项目
node10已移除

三者的典型区别可以通过一个例子看清:

// 假设项目结构:
// src/
//   utils/
//     helper.ts
//   index.ts

bundler 时,你可以不写扩展名:

// src/index.ts
import { helper } from "./utils/helper";  // ✅ bundler 能找到

node16 / nodenext 时,扩展名必须写完整——而且写的是编译产物的扩展名 .js,不是 .ts

// src/index.ts
import { helper } from "./utils/helper.js";
//                                  ^^^ 注意是 .js 不是 .ts

这是 Node.js ESM 规范的要求,一开始会觉得别扭,习惯了就好。

TypeScript 7.0 重要变更moduleResolution: "node10"(旧称 "node")已被移除。迁移时改为 "bundler""node16"

target:编译成哪个版本的 JavaScript

target 决定 tsc 输出什么语法的 JS 代码。TypeScript 7.0 把它下限砍到了 ES2015

{
  "compilerOptions": {
    "target": "ES2023"
  }
}

同一段 TypeScript 源码,不同 target 的产物差异显著:

// 源码
const greet = (name: string): string => `Hello, ${name}`;

// target: ES2023 → 原样保留(箭头函数、const、模板字符串都原生支持)
// target: ES2015 → 也原样保留(ES2015 原生支持这些语法)

7.0 不再支持 es3es5。如果你设了这两个值,tsc 会报错。

TypeScript 7.0 重要变更target 下限是 ES2015es3es5 已被彻底移除。

选 target 的简单原则:用你部署环境原生支持的版本。Node.js 22 原生支持到 ES2024,那 ES2022ES2023 就是安全的选择。浏览器端稍微保守一点,但 2026 年了,ES2020 完全可以覆盖 99% 的用户。

lib:能用哪些标准库

lib 告诉 TypeScript 在类型检查时”认得”哪些 JavaScript 内置 API。不写 lib 时,TypeScript 会根据 target 自动推导一套。

比如 target: "ES2023" 会自动包含 ES2023 相关库,让你在代码里直接用 Array.prototype.findLast 等特性而不报类型错误。

如果你在前端项目里用 document.querySelector,需要手动加上 "DOM"

{
  "compilerOptions": {
    "target": "ES2023",
    "lib": ["ES2023", "DOM"]
  }
}

常用 lib 值:

包含
"ES2023"ES2023 标准库
"ESNext"最新提案阶段的 API
"DOM"浏览器 DOM API
"DOM.Iterable"DOM 集合的可迭代接口
"WebWorker"Web Worker 环境

Node.js 后端项目通常只写 ["ES2023"],前端项目加 ["ES2023", "DOM"]

jsx:处理 JSX 语法

如果你的项目用了 React、Preact 或其他 JSX 框架,jsx 选项控制编译产物中 JSX 代码的处理方式:

行为
preserve原样保留 JSX,后续由 Babel 或打包工具处理
react-jsxReact 17+ 的自动运行时(import { jsx } from "react/jsx-runtime"
react-jsxdev同上 + 开发模式调试信息
react传统模式:React.createElement("div")
react-native保留 JSX 但不转换

现代 React 项目常见的配置:

{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

如果你用 Vite + React,打包工具自己会处理 JSX 转换,TypeScript 这边设 preserve 就够了。

outDir 和 rootDir:输出和源码分家

这两个选项配对使用,让源文件和编译产物物理分离:

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

效果:

src/
  index.ts
  utils/
    helper.ts

→ tsc →

dist/
  index.js
  utils/
    helper.js

rootDir 决定了 dist 下目录结构的”起点”。所有 src/ 开头文件的相对路径,从 src/ 开始计算,拼到 outDir 后面。

如果你不写 rootDir,TypeScript 会自动取所有输入文件的最长公共前缀目录。

Note

outDirrootDir 都不会改你的源文件。它们只影响 tsc 把产物放在哪里。

sourceMap:出错了能找到源码

TypeScript 编译产出的 JS 文件和源文件的对应关系,由 source map 来维护。

{
  "compilerOptions": {
    "sourceMap": true
  }
}

开了之后,dist/ 里每个 .js 文件旁边会多一个 .js.map 文件。浏览器或 Node.js 调试器在报错时就能直接跳到对应的 .ts 源码行,而不是让你在编译后的 JS 里摸黑调试。

这个选项几乎在所有项目里都应该打开。唯一不开的理由是你的代码完全不调试——不太现实。

declaration 和 declarationMap:产出类型声明

如果你的 TypeScript 代码要作为 npm 包发布给别人用,declaration 几乎是必开的:

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist/types"
  }
}

tsc 会为每个 .ts 文件生成对应的 .d.ts(类型声明文件)。别人安装你的包后,IDE 就能提供自动补全和类型提示。

declarationMapdeclaration 的基础上更进一步——它为 .d.ts 文件生成 source map。意义在于:别人在你的库源码里”跳转到定义”时,IDE 能直接跳到 .ts 源码而不是 .d.ts 文件。

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true
  }
}

这两个选项在写业务代码的项目里不必要,但在写库或 SDK 时价值巨大。

paths 和 baseUrl:告别 ../../../

深层嵌套目录的导入路径是最让人心烦的事情之一:

// 这种路径每次写都想摔键盘
import { formatDate } from "../../../utils/date";
import { apiClient } from "../../services/api";

paths + baseUrl 让你用干净的别名替换这些长路径:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

配置之后,导入变成:

import { formatDate } from "@utils/date";
import { Button } from "@components/Button";
import { apiClient } from "@/services/api";

注意两件事:

  1. paths 只在 TypeScript 类型检查层面生效。运行时(Node.js 或浏览器)不认识这些别名。你需要打包工具(Vite 的 resolve.alias、Webpack 的 resolve.aliastsconfig-paths)做对应的运行时映射。

  2. baseUrl 是 paths 里所有模式的解析起点。上面的例子中 baseUrl: "." 意味着 src/* 是从 tsconfig.json 所在目录开始找。

一个完整的 7.0 推荐配置

综合以上所有选项,一个典型的 TypeScript 7.0 前端项目配置大概是这样的:

{
  "compilerOptions": {
    // 语言特性
    "target": "ES2023",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",

    // 模块
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,

    // 输出
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // 严格检查(7.0 默认已开,显式写出来更清晰)
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    // 路径别名
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },

    // 互操作
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

这几乎能覆盖 90% 的业务项目需求。你不用背——建个模板项目,以后从这份配置改起就行。

小结

这一章把 compilerOptions 的核心选项逐个过了一遍。关键点:

  • strict: true 挡住 8 类常见类型隐患,7.0 默认就已开启
  • modulemoduleResolution 是一对搭档:前端用 esnext + bundler,Node.js 用 node16 + node16
  • target 不要设置低于 ES2015(7.0 也不让),根据部署环境选 ES2022/ES2023
  • outDir / rootDir 把源码和产物分干净
  • paths + baseUrl 告别深层相对路径,但运行时需要打包工具配合
  • declaration + declarationMap 发布 npm 包时几乎必开

下一章带你看 TypeScript 7.0 本身有哪些新东西——Go 移植、10 倍性能、并行编译,以及那些被砍掉的旧选项。