写在前面:为什么这个流程很重要
说实话,很多开发者(包括我曾经)在搭建 TypeScript 项目时,要么直接复制网上过时的 tsconfig.json,要么 ESLint 配置一堆规则互相打架,最后跑起来报错连天,维护起来更是灾难。
这篇文章不是那种“复制粘贴就能用”的速成指南,而是基于 2024-2025 年最新工具链(TypeScript 5.x+、ESLint 9.x Flat Config、Prettier 3.x)的实战手册。我会用最直白的语言,配合完整可运行的代码示例,带你一步步把项目骨架搭得稳稳的。
第一部分:tsconfig.json 的正确打开方式
1.1 基础结构:先别急着复制
很多教程上来就给你一堆 strict: true、esModuleInterop: true,但从不解释为什么。我们先从一个最小可用的配置开始,理解每个选项的作用。
{
"compilerOptions": {
/* 目标与模块 */
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
/* 输出 */
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
/* 严格模式 */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
/* 互操作性 */
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
/* 检查 */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noEmit": false,
"sourceMap": true,
"removeComments": false,
/* 路径别名(可选,但强烈推荐)*/
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
1.2 逐个拆解:这些选项到底在说什么
target vs module 的关系
target 决定 TypeScript 编译输出什么版本的 JavaScript,module 决定使用哪种模块系统。
// target: "ES2022" 会输出:
const result = someArray?.filter(Boolean); // 可选链,ES2020+ 支持
// 如果 target: "ES2015",同样的代码会变成:
var _a;
const result = (_a = someArray) === null || _a === void 0 ? void 0 : _a.filter(Boolean);
NodeNext 模块解析
如果你是用 Node.js 运行 TypeScript 编译后的代码,NodeNext 是最佳选择。它能正确处理 package.json 中的 "type": "module",避免“Cannot find module”的经典错误。
// package.json 中必须有这行
{
"type": "module"
}
严格模式的“副作用”
开启 strict: true 后,你会突然发现代码“变难写”了:
// 之前能跑,现在报错
function getUser(id: number) {
return { id: id, name: "张三" }; // ✅ 没问题
}
// 之前能跑,现在报错
let value = null; // ❌ 报错:TS2322: Type 'null' is not assignable to type 'unknown'
value = "hello"; // 赋值失败
// 修复方式
let value: string | null = null;
这就是 TypeScript 的价值:提前发现错误,而不是运行时崩溃。
noUnusedLocals 和 noUnusedParameters
这两个选项会让你非常“痛苦”,但它们能帮你发现僵尸代码:
// 这两个参数明明没用到,留着干嘛?
function processUser(id: number, username: string, email: string) {
console.log(`用户ID: ${id}`);
// email 完全没用到,开启 noUnusedParameters 后会报错
}
// 修复:用下划线前缀表示“故意忽略”
function processUser(id: number, _username: string, _email: string) {
console.log(`用户ID: ${id}`);
}
1.3 路径别名:让 import 路径更优雅
// 没有路径别名时
import { UserService } from "../../services/user.service";
// 有路径别名后
import { UserService } from "@/services/user.service";
在 tsconfig.json 中配置:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
}
}
注意:路径别名只在 TypeScript 编译时有效,运行时需要 moduleResolution: "NodeNext" 配合,或者使用 tsconfig-paths 包(Node.js 运行时)。
第二部分:ESLint 9.x Flat Config 完全指南
2.1 为什么 ESLint 9 变了?
ESLint 9 引入了 Flat Config(扁平配置),eslintrc 格式(.eslintrc.js、.eslintrc.json)被标记为废弃。这不是噱头,而是为了解决长期存在的配置混乱问题。
旧的配置方式:
// .eslintrc.json(旧方式,已废弃)
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
新的配置方式:
// eslint.config.js(新方式,推荐)
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist", "node_modules", "**/*.d.ts"] },
{
files: ["**/*.ts", "**/*.tsx"],
extends: [
tseslint.configs.recommended,
tseslint.configs.strict,
],
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}]
}
}
);
2.2 完整 ESLint 配置示例
// eslint.config.js
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";
import importPlugin from "eslint-plugin-import";
import jestPlugin from "eslint-plugin-jest";
export default tseslint.config(
// 全局忽略规则
{
ignores: [
"dist/**/*",
"node_modules/**/*",
"**/*.d.ts",
"**/*.config.js",
"**/*.config.ts",
],
},
// 基础 JavaScript 规则
eslint.configs.recommended,
// TypeScript 推荐配置
...tseslint.configs.recommended,
// TypeScript 严格配置(可选,更严格)
// ...tseslint.configs.strict,
// Prettier 集成(关闭与 Prettier 冲突的规则)
prettier,
// Import 插件配置
{
plugins: {
import: importPlugin,
},
rules: {
"import/order": [
"error",
{
groups: [
"builtin",
"external",
"internal",
["parent", "sibling", "index"],
],
"newlines-between": "always",
alphabetize: { order: "asc", caseInsensitive: true },
},
],
},
},
// 文件级别配置
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
rules: {
// 禁止使用 any
"@typescript-eslint/no-explicit-any": "error",
// 禁止非空断言(!)
"@typescript-eslint/no-non-null-assertion": "warn",
// 禁止多余的 non-null 断言
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
// 变量未使用警告
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
// 强制使用 async/await
"require-await": "error",
// 禁止 Promise 忽略
"no-return-await": "error",
},
},
// 测试文件配置
{
files: ["**/*.test.ts", "**/*.spec.ts"],
plugins: {
jest: jestPlugin,
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
// 忽略 dist 目录
{
files: ["dist/**/*"],
rules: {
// 不检查构建产物
},
}
);
2.3 常见 ESLint 规则详解
no-explicit-any vs no-unsafe-assignment
// ❌ 错误:使用 any
function process(data: any) {
return data.value;
}
// ✅ 正确:使用 unknown,再类型守卫
function process(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: string }).value;
}
throw new Error("Invalid data");
}
no-non-null-assertion
interface User {
name: string;
age?: number;
}
const user: User | null = getUser();
// ❌ 错误:非空断言
console.log(user!.name);
// ✅ 正确:可选链 + 默认值
console.log(user?.name ?? "Unknown");
// ✅ 正确:类型守卫
if (user !== null && user !== undefined) {
console.log(user.name);
}
第三部分:Prettier 配置与集成
3.1 为什么要 Prettier?
ESLint 负责代码质量(有没有 bug),Prettier 负责代码格式(长什么样)。两者配合,才能既干净又美观。
3.2 完整 Prettier 配置
// prettier.config.js
export default {
// 每行最大字符数
printWidth: 100,
// 缩进空格数
tabWidth: 2,
// 使用空格而非缩进
useTabs: false,
// 语句末尾加分号
semi: true,
// 使用单引号
singleQuote: true,
// 尾逗号(多行时)
trailingComma: "es5",
// 对象大括号后加空格
bracketSpacing: true,
// JSX 括号不另起一行
bracketSameLine: false,
// 箭头函数单参数不加括号(可选)
arrowParens: "avoid",
// 解析器
parser: "typescript",
// 文件末尾不换行
endOfLine: "lf",
};
3.3 与 ESLint 的集成
关键一点:不要同时让 ESLint 和 Prettier 格式化代码。ESLint 有 prettier/prettier 规则,让 ESLint 负责调用 Prettier,这样格式化只需要运行一次。
// eslint.config.js 中添加
import prettier from "eslint-config-prettier";
import prettierPlugin from "eslint-plugin-prettier";
export default tseslint.config(
// ... 其他配置
// 关闭与 Prettier 冲突的规则
prettier,
// 启用 Prettier 规则
{
plugins: {
prettier: prettierPlugin,
},
rules: {
"prettier/prettier": "error",
},
}
);
第四部分:package.json 脚本整合
{
"name": "typescript-boilerplate",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist",
"prepare": "husky install",
"test": "vitest",
"test:run": "vitest run"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"prettier": "^3.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vitest": "^1.0.0"
}
}
lint-staged 配置:Git 提交前自动检查
// lint-staged.config.js
export default {
"**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
],
"**/*.{json,md}": [
"prettier --write",
],
};
第五部分:常见坑点与避坑指南
5.1 坑点一:模块解析错误
现象:运行时找不到模块,或者编译时报 Cannot find module。
原因:moduleResolution 配置不当。
{
"compilerOptions": {
"moduleResolution": "NodeNext" // ✅ 推荐:与 Node.js 原生 ESM 兼容
// "moduleResolution": "node" // ❌ 旧方式,不推荐
// "moduleResolution": "bundler" // ⚠️ 适合 Vite/Webpack,不适合 Node.js
}
}
解决方案:
- 确保
package.json中有"type": "module" - 使用
moduleResolution: "NodeNext" - 导入时使用完整扩展名(某些情况下)
// ❌ 可能报错
import { something } from "./utils";
// ✅ 更可靠
import { something } from "./utils.js";
5.2 坑点二:strictNullChecks 带来的痛苦
现象:代码到处报错,尤其是 undefined is possibly 'null'。
原因:开启了严格空值检查,但没有正确处理可选属性。
解决方案:
interface Config {
timeout?: number;
retries?: number;
}
// ❌ 错误写法
function createConfig(config: Config) {
const timeout = config.timeout || 5000; // 如果 timeout 是 0,会被错误处理
const retries = config.retries ?? 3; // ✅ 正确:nullish coalescing
}
// ✅ 正确写法
function createConfig(config: Config) {
const timeout = config.timeout ?? 5000; // 只处理 null/undefined
const retries = config.retries ?? 3;
}
可选链 vs 非空断言:
// ❌ 不推荐:非空断言掩盖了潜在问题
const name = user!.name;
// ✅ 推荐:可选链 + 默认值
const name = user?.name ?? "Unknown";
// ✅ 或者:类型守卫
if (user !== null && user !== undefined) {
const name = user.name;
}
5.3 坑点三:ESLint 与 Prettier 冲突
现象:ESLint 说格式正确,Prettier 说需要格式化,或者反之。
原因:没有正确集成两者,或者 ESLint 配置中有重复的规则。
解决方案:
- 始终使用
eslint-config-prettier关闭冲突规则 - 使用 `eslint
