哎,说实话,每次打开一个新的 TypeScript 项目,看着那一堆 tsconfig.json、vite.config.ts、eslint 还有 prettier 的配置,我都能感觉到那种熟悉的“头大”。想当年,我还是个小菜鸟的时候,为了配一个正确的 tsconfig 熬了三个通宵,结果跑起来还是报错,那种挫败感至今记忆犹新。
但今天,我不想给你整那些枯燥的“第一步、第二步”,咱们就像在咖啡馆聊天一样,把那些坑一个个填平,顺便讲讲背后的逻辑。毕竟,懂原理才能不踩坑,对吧?
先聊聊:为什么我们这么执着于 TypeScript?
在动手写代码之前,咱们得先统一思想。你可能觉得,JavaScript 多好啊,灵活、随意、想到哪写到哪。确实,在项目初期或者写个 Demo 的时候,JS 的快感是无可比拟的。
但是,当你面对一个几十万行代码的中大型项目时,没有类型检查就像是在走钢丝还没有安全绳。TypeScript 的本质是什么?是契约。
// 这是 JS 的思维
function getTotal(price, count) {
return price * count;
}
// 这是 TS 的思维
function getTotal(price: number, count: number): number {
return price * count;
}
你看,第二个版本虽然多写了几个字符,但它告诉了你(和每一个读你代码的人):price 和 count 必须是数字,返回的也必须是数字。如果别人传了个字符串进来,编译器立马就给你亮红灯,而不是等到运行时页面崩了才知道错了。
所以,咱们要搭建的这个项目,不仅仅是一个能跑的程序,更是一个健壮、可维护、易于协作的工程。
项目初始化:选对武器,事半功倍
现在的前端工具链百花齐放,Webpack、Rollup、Vite……对于新手来说,选择困难症都要犯了。我强烈建议你从 Vite 开始。
为什么?因为快。快到让你怀疑人生。以前冷启动 Webpack 项目要几十秒,Vite 几乎是毫秒级。这对于我们频繁调试、快速迭代的学习过程来说,体验简直是降维打击。
咱们用 npm create vite@latest 来创建项目。这里有个小细节,你可能已经注意到了,Vite 官方模板现在默认就提供了 TypeScript 版本。
npm create vite@latest my-ts-project -- --template react-ts
# 如果你用 Vue,就把 react-ts 换成 vue-ts
# 如果你用 Svelte,就换成 svelte-ts
进入目录,安装依赖,跑起来:
cd my-ts-project
npm install
npm run dev
看到浏览器里渲染出的 React/Vue 欢迎页面了吗?恭喜你,基础环境已经到位。但这只是“能跑”,离“专业”还差得远呢。接下来,我们要开始填坑了。
tsconfig.json:核心配置的深度解析
这是本文的重点,也是大多数人容易忽略的地方。很多人直接从网上抄一份配置,改改路径就用了,结果遇到奇怪的问题抓瞎。咱们来看看每个关键字段背后的含义。
1. compilerOptions:编译器的灵魂
打开项目根目录的 tsconfig.json,你会看到一堆选项。别慌,咱们一个个拆解。
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
target 和 lib 的关系
target 决定了 TypeScript 代码会被编译成哪个版本的 JavaScript。现在的趋势是往高走,ES2020 或者 ES2022 都是不错的选择。它支持了 Optional Chaining (?.)、Nullish Coalescing (??) 等现代特性。
lib 则告诉编译器你运行环境的类型定义在哪里。比如 DOM 表示你写的是浏览器端代码,DOM.Iterable 支持 for...of 遍历 DOM 节点。如果你不小心漏掉了 DOM,那你连 document.getElementById 都会报红。
避坑指南:不要把 target 设置得太低,也不要设置得比你的 lib 还高。如果 target 是 ES2020,但你没加 ES2020 到 lib 里,编译器不知道这个版本有哪些 API,就会报错。
module 和 moduleResolution
这是最容易混淆的一对。
module:指定输出模块的格式。Vite 是模块打包器,它原生支持 ES Modules (ESM),所以这里填ESNext是最合适的。moduleResolution:告诉编译器如何解析import语句。Vite 项目推荐用bundler,因为它模拟了 Vite 解析模块的行为,比传统的node或node16更灵活,尤其是处理一些别名和路径映射时。
为什么不用 node? node 模式是为 CommonJS (CJS) 设计的,它假设你在 Node.js 环境运行,对 ESM 的支持并不完美。在 Vite 项目里用 node 解析模块,你可能会遇到“找不到模块”的假报错。
strict: true
这是我最想强调的一个选项。永远开启它!
strict 是一个总开关,它背后包含了十几个具体的检查规则,比如:
noImplicitAny: 不允许隐式的any类型。strictNullChecks: 严格的空值检查。null和undefined不再是所有类型的子类型,你必须显式处理它们。noImplicitReturns: 函数必须有返回值,或者所有分支都有返回。
很多老项目没开 strict,是因为历史包袱太重,强行开启会爆出一万多个错误。但在新项目里,请毫不犹豫地把它打开。这是保证代码质量的底线。
noEmit: true
Vite 是开发服务器,它通过 esbuild 实时编译 TypeScript,不需要生成 .js 文件。所以,我们告诉 TypeScript 编译器:“你只管检查类型,不要输出任何文件。” 这样能节省大量的磁盘 I/O 操作,提升构建速度。
isolatedModules: true
这个选项是为了配合 babel 或 swc 等单文件编译器存在的。它要求每个文件都被独立解析,不能依赖文件之间的上下文关联。Vite 的单文件编译模式正好符合这个要求。开启它可以让一些更严格的检查提前暴露问题。
2. include 和 exclude
"include": ["src", "vite.config.ts"],
"exclude": ["node_modules"]
这一行看似简单,实则重要。它指定了 TypeScript 编译器要处理哪些文件。
常见错误:很多新手发现 node_modules 里的某个库类型报错,下意识地把 node_modules 从 exclude 里删掉,结果编译慢到爆炸。记住,node_modules 永远不要进 include,除非你有特殊需求(比如你在写一个类型测试库)。
代码规范:ESLint 和 Prettier 的双重守护
代码写得对,还要写得漂亮。ESLint 负责检查逻辑错误和潜在 bug,Prettier 负责统一代码格式。这两者经常产生冲突,比如 Prettier 想把一行代码换行,ESLint 却要求保持单行。
1. 安装依赖
npm install -D eslint prettier eslint-config-prettier eslint-plugin-prettier
这里有个小技巧:eslint-config-prettier 的作用是把 ESLint 中与 Prettier 冲突的规则关掉。eslint-plugin-prettier 则是把 Prettier 作为一个 ESLint 规则来运行。这样,Prettier 的格式错误也能被 ESLint 一次性报出来,方便你修复。
2. 创建配置文件
在项目根目录创建 .eslintrc.cjs:
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended', // 关键:把 Prettier 集成进来
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
rules: {
// 这里可以自定义你的规则
'@typescript-eslint/no-explicit-any': 'warn', // 警告,不要禁止
'@typescript-eslint/ban-ts-comment': 'warn',
},
};
再创建一个 .prettierrc:
{
"singleQuote": true,
"semi": false,
"printWidth": 100,
"trailingComma": "es5"
}
看,singleQuote 和 semi: false 是现在非常流行的风格,简单清爽。
3. 集成到 VS Code
光有配置文件还不够,你得让编辑器自动帮你格式化。在 VS Code 的 .vscode/settings.json 里加上:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
这样,你每次按下 Ctrl+S 保存文件,VS Code 就会自动调用 Prettier 格式化,并运行 ESLint 修复一些简单的问题。
避坑指南:如果保存后格式没变,检查一下你是否安装了 Prettier - Code formatter 插件,并且是否正确设置了默认格式化器。有时候,项目里混用了 eslint-config-airbnb 之类的配置,也会和 Prettier 产生奇妙的化学反应(通常是坏事),记得用 eslint-config-prettier 把它们关掉。
路径别名:告别深层嵌套的 import
不知道你有没有这种感觉:
// 这看起来太丑了,对吧?
import { Button } from '../../../../components/Button';
在大型项目里,文件层级可能深达五六层,这种 import 语句简直是噩梦。而且,如果你移动了组件的位置,就得手动改所有引用它的路径。
为了解决这个问题,我们需要配置路径别名。
1. TypeScript 配置
在 tsconfig.json 的 compilerOptions 里加上 paths:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
这告诉 TypeScript:“当你看到 @/ 开头的路径,就去 src/ 下面找。”
2. 构建工具配置
TypeScript 只是处理类型检查,实际打包时,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'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
},
},
});
3. 验证
现在,你可以这样写了:
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';
是不是清爽多了?而且,移动组件时,只需要改一个地方,或者 VS Code 的重构功能能帮你自动更新。
自动化工具:Husky 和 lint-staged
代码规范不能只靠自觉。我们要用工具来强制约束。想象一下,如果有人提了一个 PR,代码格式乱成一团,ESLint 检查也没过,你是不是很想打人?
Husky 和 lint-staged 就是来干这个的。
1. 安装
npm install -D husky lint-staged
npx husky-init && npm install
2. 配置 hook
Husky 会在 .husky/pre-commit 生成一个钩子脚本。我们编辑它,加入 lint-staged 的配置:
npx lint-staged
然后在 package.json 里添加:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
这有什么好处? 默认情况下,lint-staged 只对你 git add 的文件进行检查。如果你一次提交了 100 个文件,ESLint 只需要跑这 100 个文件,而不是整个项目。这极大地提升了提交速度。
而且,--fix 参数会让 ESLint 自动修复一些格式问题(比如分号、引号),而 Prettier 会把剩余的所有格式统一。这样,任何进入仓库的代码,都必须是干净、规范的。
避坑指南:如果你的项目里有 public 目录或者测试文件,记得在 lint-staged 的配置里排除它们,或者确保你的 ESLint 配置能正确处理这些文件。
类型定义:如何处理第三方库的类型?
这是 TypeScript 项目中另一个大坑。很多第三方库可能没有类型定义,或者类型定义不完整。
1. 已经有 @types 包
对于常见的库,比如 react、lodash,社区已经提供了 @types/react、@types/lodash 这样的包。直接 npm install -D @types/xxx 即可。
2. 没有类型定义的库
如果你用的库没有类型定义,TypeScript 会报错:“Cannot find module ‘xxx’”。
这时候你有两个选择:
选择 A:安装 @types/xxx(如果存在)
有时候,官方没发布,但社区有人维护了。先去 npm search @types/xxx 看看。
选择 B:自己写声明文件
如果确定没有类型定义,可以在 src/types 目录下创建一个 .d.ts 文件。
// src/types/custom-lib.d.ts
declare module 'custom-lib' {
export function doSomething(): void;
export const VERSION: string;
}
或者,如果你实在懒得写,可以暂时妥协:
import * as customLib from 'custom-lib';
// 强制转换为 any
const lib = customLib as any;
lib.doSomething();
不推荐长期使用 any,因为这等于放弃了类型检查的保护。尽量在后续补充完整的类型定义。
环境变量:敏感信息要藏好
开发环境和生产环境往往有不同的配置,比如 API 地址、密钥等。在 TypeScript 项目里处理环境变量需要一点技巧。
1. 定义类型
TypeScript 不会自动识别 import.meta.env 的所有属性。你需要自己声明一下。
在 src/vite-env.d.ts 里:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_APP_TITLE: string;
// 添加其他你需要的环境变量
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
2. 使用 .env 文件
在项目根目录创建 .env.development 和 .env.production:
# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=My App (Dev)
# .env.production
VITE_API_BASE_URL=https://api.myapp.com
VITE_APP_TITLE=My App
重要提示:务必把 .env 文件加入 .gitignore,不要把密钥提交到代码仓库里!
最佳实践总结:一些血泪教训
聊了这么多配置,最后我想分享几个在实际项目中总结出来的最佳实践,希望能帮你少走弯路。
不要滥用
anyany是 TypeScript 的毒药。它会让类型检查失效,让你退回到 JavaScript 的裸奔状态。如果实在不知道类型是什么,先用unknown,然后再做类型守卫判断。function handleData(data: unknown) { if (typeof data === 'object' && data !== null && 'id' in data) { // 在这里,TypeScript 知道 data 是有 id 属性的对象 console.log((data as { id: number }).id); } }善用
interface和typeinterface更适合描述对象的结构,支持继承和合并。type更灵活,可以定义联合类型、交叉类型、元组等。- 对于 API 返回的数据结构,我推荐用
type,因为它更直观,不容易出错。
组件 Props 的类型要精准 不要给组件的 Props 用
any或者object。要拆分成具体的属性类型。如果 Props 很多,考虑用interface来定义,并导出,方便其他地方复用。定期运行 ESLint 和 Prettier
