带过几届新人之后我发现一个挺反直觉的现象:真正把初学者劝退的,往往不是语法本身,而是第一次打开编辑器之后的那半小时。VSCode 装好了,TypeScript 也听说过,但 Node 是什么关系、tsc 为什么敲了没反应、报红的一堆波浪线到底该不该管,这些问题凑在一起,很容易让人误以为自己不适合写代码。我写这篇东西的目的很直接,就是把 VSCode 搭建 TypeScript 环境 这件事从头到尾拆开讲一遍,包括每一步背后为什么要这么做,以及我在实际带人过程中收集到的坑。
这篇内容适合三类人:完全没碰过 Node 生态的前端新手;写了几年 JavaScript 想切到 TypeScript 但一直被配置劝退的老手;以及需要给团队新人写一份能直接照抄的入门文档的人。整套流程在 Windows、macOS、Linux 上都能跑通,差异点我会单独标出来。全程不依赖任何收费工具,装完之后你拿到的是一个能补全、能报错、能断点调试、能一键编译的最小可用环境,剩下的框架和工程化都可以往上叠。
1. 先把地基打好:为什么是 VSCode 加 TypeScript 这套组合
动手之前先花五分钟把"为什么"想清楚,比闷头装十个插件有用得多。我见过太多人环境装得花里胡哨,结果连 tsc 和 ts-node 的区别都说不清,一遇到报错就抓瞎。这一节不写操作,只讲判断依据,想直接动手的可以跳到第二节。
1.1 TypeScript 到底补上了 JavaScript 的哪个缺口
JavaScript 是动态类型语言,变量的类型在运行时才确定。写小脚本的时候这很爽,随手let a = 1就完事;但项目一旦超过几百行,问题就来了。你把某个函数的参数从userName改名成username,编辑器不会提示你还有三个调用点没改,只有等用户点进那个页面、控制台抛出Cannot read property of undefined,你才知道出事了。这种错误在 JavaScript 里是常态,而 TypeScript 的价值就是把这类错误从"运行时"提前到"敲代码时"。
打个比方,JavaScript 像一箱没有标签的零件盒,你凭记忆知道哪个螺丝配哪个孔;TypeScript 则是给每个零件贴了标签,还配了一张装配图。标签不会让零件更好用,但它能让你在拿错的时候立刻察觉。TypeScript 在编译阶段做静态类型检查,编辑器里的波浪线、红色提示、悬停时的类型信息,全都来自它。
代价当然也有。你需要多写类型标注,需要多一个编译步骤,需要理解接口、泛型、类型收窄这些概念。所以我的建议很明确:几十行的脚本、一次性的数据处理任务,直接用 JavaScript 更省事;只要这个项目你打算维护超过一个月,或者会有第二个人来看你的代码,TypeScript 的投入产出比就立刻转正了。
1.2 为什么我最终把 VSCode 定为默认编辑器
编辑器选择是个老话题,我的结论是:对绝大多数人来说 VSCode 是最没争议的答案,原因不在功能多少,而在"零配置起步"这四个字。TypeScript 是微软主导的语言,VSCode 也是微软的产品,所以语言服务是内置的。你写完npm install typescript之后,什么都不用配,VSCode 就会自动识别项目里的 TypeScript 版本,补全、跳转定义、重命名符号、查找所有引用这些功能全部开箱可用。
对比一下其他选择:WebStorm 的智能提示确实更细腻,重构能力也强,但它是收费软件,启动慢,吃内存也凶,对刚入门的人来说有点重;Sublime Text 启动飞快,可它的 TypeScript 支持基本靠社区插件拼装,配置成本反而更高;Vim 和 Neovim 那一套,除非你本来就习惯了模态编辑,否则前期学习成本会盖过一切收益。
还有一个容易被忽视的点:VSCode 的远程开发体验做得很完整。如果你在公司用的是 Windows 但代码跑在 Linux 服务器上,可以直接用 Remote 相关功能连过去,在远端目录里编辑,语言服务也跑在远端,本地机器完全不卡。这套工作流我在好几家公司都用过,稳定性没问题。新手暂时用不上,但知道有这么条路,将来遇到场景不会慌。
1.3 开工前的三项准备和两个禁忌
第一项准备是装 Node.js。很多人会问:我就想学 TypeScript,为什么非要装 Node?道理很简单,TypeScript 的编译器tsc本身就是一个用 JavaScript 写的程序,它需要 Node 运行时才能跑起来;而npm(Node 自带的包管理器)是你安装 TypeScript、安装类型声明文件的唯一入口。没有 Node,后面所有命令都执行不了。
第二项准备是规划项目目录。这里有两个我必须强调的禁忌:一是路径里不要出现中文和空格,Windows 上某些工具链对非 ASCII 路径处理得不好,会出现莫名其妙的乱码或找不到文件的报错;二是不要把项目放在 OneDrive、iCloud、坚果云这类同步盘目录里,文件监听和同步进程会打架,表现为编辑器反复重新加载、tsc --watch频繁触发编译,甚至出现文件被锁定的情况。我自己的习惯是在用户目录下建一个纯英文的code文件夹,所有练习项目都往里放。
第三项准备是心态。TypeScript 报错密集,尤其第一次打开strict模式的时候,一个刚写完的文件可能瞬间飘出十几条红线。这不是你写错了,这是它在帮你排查。学会读报错、理解报错,是这段学习里最有价值的部分,比背语法有用得多。
2. 从零安装到项目跑起来:完整配置流程
这一节是实操部分,我会把每个命令的作用和每个配置项的取舍都讲清楚。你可以照着敲,但建议边敲边看解释,否则下次换个项目你还是不知道怎么配。
2.1 装 Node.js 并验证版本
去 Node.js 官网下载 LTS(长期支持)版本。为什么是 LTS 而不是 Current?因为 LTS 版本的依赖生态适配最完整,各种工具链都测过,出问题的概率最低。下载完成后一路默认安装即可,Windows 用户注意安装向导里有个 "Add to PATH" 的勾选项要保证是勾上的,否则命令行里敲 node 会提示找不到命令。
装完打开终端(Windows 用 PowerShell 或 CMD,macOS 用 Terminal),执行:
node -v npm -v正常的话会分别输出类似v20.11.1和10.2.4的版本号。这里有个细节:Node 的版本号规则是主版本.次版本.补丁。主版本偶数的是 LTS 线,奇数是过渡版本,生命周期短。如果你是团队协作,最好和同事对齐主版本号,不然容易在依赖安装上出现差异,这就是业内常说的"在我机器上是好的"的经典来源。
如果你需要在多个 Node 版本之间切换(比如同时维护两个要求不同版本的老项目),可以再装一个版本管理工具,Windows 上是 nvm-windows,macOS 和 Linux 上是 nvm。安装前记得先卸载干净已装的 Node,否则会冲突。新手可以先跳过这一步,等项目多了再说。
2.2 VSCode 的下载安装与界面汉化
VSCode 官网下载对应系统的安装包。Windows 安装过程中有几个选项值得勾上:把"通过 Code 打开"添加到文件资源管理器右键菜单、把 VSCode 注册为常见文件类型的编辑器。这两个能让后续操作顺手很多。macOS 用户把下载的 app 拖进应用程序文件夹就行,然后建议在命令面板里执行一次安装code命令行工具的操作,这样在终端里可以直接用code .打开当前目录。
界面汉化有两种做法,效果一样,看你习惯哪种。第一种是启动 VSCode 后按Ctrl+Shift+P(macOS 是Cmd+Shift+P)打开命令面板,输入Configure Display Language,选择中文,它会提示你安装中文语言包,确认后重启即可。第二种是直接在扩展市场搜索 "Chinese (Simplified) Language Pack",装完重启。我个人更推荐第二种,因为你能顺便熟悉扩展面板的位置。
这里插一句:汉化只是个人偏好,不影响任何功能。我建议新手先用中文界面把概念建立起来,等熟悉了再切回英文。原因很简单,绝大多数报错信息和官方文档都是英文的,长期看英文界面能帮你形成条件反射,看到Cannot find module就知道是模块找不到,不需要在脑子里翻译一遍。
2.3 插件清单:装什么,以及为什么可以不装
扩展市场里 TypeScript 相关插件多到吓人,但真正必要的不多。下面这张表是我给新人列的清单,分了三档,按需取用。
| 插件名 | 作用 | 建议 |
|---|---|---|
| Chinese (Simplified) Language Pack | 界面汉化 | 新手推荐 |
| ESLint | 代码质量检查 | 必装 |
| Prettier | 代码格式化 | 必装 |
| Error Lens | 把报错直接显示在行尾 | 强烈推荐 |
| Path Intellisense | 路径自动补全 | 推荐 |
| GitLens | Git 历史与行内注解 | 推荐 |
| Code Spell Checker | 英文拼写检查 | 可选 |
| 各类主题、图标包 | 视觉美化 | 随意 |
需要说明的是,VSCode 内置的 TypeScript 语言服务已经提供了补全、跳转、重构、查找引用这些核心能力,不需要再额外装任何"TypeScript 支持"的插件。市面上一些号称增强提示的插件,本质是在内置能力上加壳,装多了反而会出现两个语言服务打架、提示重复的情况。我踩过这个坑:同时装了三个提示类插件,结果保存一次代码要等两秒才更新波浪线,卸掉之后立刻恢复流畅。
Error Lens 这个插件我特别想说一下,它把类型错误、ESLint 警告直接渲染在代码行末尾,不用把鼠标悬停上去就能看见。对新手理解类型系统帮助极大,你写const n: number = "abc",它会当场告诉你不能把字符串赋给数字类型,学习反馈是实时的。
2.4 项目初始化与 tsconfig.json 逐项拆解
找一个人英文路径的文件夹,在里面打开终端,依次执行:
mkdir ts-demo cd ts-demo npm init -y npm install --save-dev typescript npx tsc --init四条命令的意思分别是:建目录、进入目录、生成默认的 package.json、把 TypeScript 作为开发依赖装到本地、生成一份带注释的 tsconfig.json 模板。这里我特意用了--save-dev而不是全局安装,原因很重要:全局安装的版本对所有项目都是一个,而不同项目对 TypeScript 版本的要求可能不一样;本地安装则每个项目自带一份,版本隔离,换电脑或者换同事也能保证一致。用npx tsc而不是直接tsc,就是为了强制使用项目内的那一份。
生成的 tsconfig.json 会有一大堆被注释掉的选项,我建议全部删掉,换成下面这份精简版,然后逐条理解:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "rootDir": "./src", "outDir": "./dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "sourceMap": true, "declaration": false, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }target决定编译后代码的语法版本。设成 ES2022 意味着现代语法原样保留,运行时要求 Node 16 以上。设太低会做大量降级转换,代码变丑;设太高则可能在老环境跑不起来。做 Node 服务端开发,跟着你的 Node 版本走就行,Node 20 对应 ES2022 是安全的。
module和moduleResolution涉及模块系统,是新手最容易糊的地方。简单说,Node 生态有两套模块规范:老的 CommonJS(require)和新的 ES Module(import)。NodeNext的含义是"跟着 package.json 里的 type 字段走",如果你的 package.json 里有"type": "module",就按 ES Module 处理,否则按 CommonJS。我的建议是新建学习项目就用NodeNext,配上"type": "module",因为这是当前的方向。
rootDir和outDir分别指定源码目录和输出目录。指定它们的好处是编译后的目录结构可预测,不会出现dist/src/index.js这种多套一层的情况。如果你不指定,TypeScript 会自己推断,推断结果有时候和你想的不一样,尤其是当你把配置文件放进了被 include 的目录里。
strict是重头戏,它是一个总开关,一次性打开包括strictNullChecks、noImplicitAny、strictFunctionTypes在内的一整组严格检查。我的态度很坚决:新项目一律开,不要关。关掉之后null和undefined会悄悄混进你的类型里,等于把 TypeScript 最值钱的那部分能力扔了。老项目迁移可以先用strict: false过渡,之后逐个文件开启。
skipLibCheck: true是跳过对node_modules里.d.ts文件的类型检查。这能让编译速度明显变快,代价是第三方库的类型错误会被忽略。实际项目里几乎没人关掉它,因为第三方类型声明出错你也没法改。
sourceMap: true会生成.js.map文件,把编译后的代码映射回源码。这是断点调试能正常工作的前提,必须开。没有它,你在编辑器里打断点,运行时停在编译后的乱码上,根本没法看。
3. 写出第一个能跑的项目:编译、运行与调试三条链路
配置到位之后,真正的乐趣才开始。这一节我会带你写完一个带类型的小模块,然后分别用三种方式跑起来,最后配好断点调试。走完这一遍,你对 TypeScript 的运行机制就有了直觉。
3.1 三种运行方式怎么选
同样一段 TypeScript 代码,有三种跑法,各有适用场景:
| 方式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| tsc + node | 先编译成 JS,再用 Node 执行 | 最贴近生产环境,产物可见 | 多一步,每次改代码要重编译 | 打包发布、排查编译期问题 |
| ts-node | 运行时即时编译 | 一条命令直接跑 TS | 启动慢,类型检查可跳过 | 临时脚本、学习练手 |
| tsx | 基于 esbuild 转译 | 启动快,体验接近原生 | 不做类型检查,只擦除类型 | 本地开发热重载 |
我的实际用法是这样的:学习阶段用 tsx 跑得快,改完立刻见效;需要验证类型错误的时候手动跑一次tsc --noEmit;准备部署的时候用 tsc 编译出 dist 目录。ts-node 现在我用得少了,主要是启动速度确实比 tsx 慢一截,尤其是项目大了之后。
安装方式:
npm install --save-dev tsx如果你还想试试 ts-node:
npm install --save-dev ts-node @types/node注意@types/node这个包,它提供了 Node 内置模块(比如fs、path)的类型声明。不装的话,你import fs from "fs"会直接报找不到模块声明。它是纯类型包,编译后不产生任何运行时代码,所以装成开发依赖就行。
3.2 手写一个带类型的模块并编译
在项目根目录建src文件夹,写两个文件。先写src/types.ts:
export interface Task { id: number; title: string; done: boolean; tags?: string[]; } export type TaskFilter = "all" | "active" | "done";再写src/index.ts:
import { Task, TaskFilter } from "./types.js"; const tasks: Task[] = [ { id: 1, title: "装好 Node 和 VSCode", done: true, tags: ["环境"] }, { id: 2, title: "写出第一个接口", done: false }, ]; function filterTasks(list: Task[], filter: TaskFilter): Task[] { if (filter === "all") return list; if (filter === "done") return list.filter((t) => t.done); return list.filter((t) => !t.done); } function format(t: Task): string { const mark = t.done ? "[x]" : "[ ]"; const tags = t.tags?.length ? ` #${t.tags.join(" #")}` : ""; return `${mark} #${t.id} ${t.title}${tags}`; } const result = filterTasks(tasks, "all"); result.forEach((t) => console.log(format(t)));这里有三个细节值得停下来看。第一,import的路径写了./types.js而不是./types,也没有写.ts。这看着别扭,但它是 Node ESM 的硬性要求:运行时必须带扩展名,而运行时看到的是编译后的.js文件,所以源码里就得写.js。TypeScript 会在编译时把它解析到对应的.ts文件上。这个规则是新手第一天最容易撞墙的地方,后面常见问题部分我会再展开。
第二,TaskFilter用的是联合类型而不是枚举。联合类型零运行时开销,编辑器还能精确推断分支,if (filter === "done")这句之后它会自动把类型收窄成"done"。枚举编译后会生成额外对象,非必要我一般不用。
第三,t.tags?.length里的问号是可选链,配合strict模式下的空值检查,能安全处理tags可能不存在的情况。如果没有这个问号,TypeScript 会直接提示你tags可能是 undefined,不允许你访问.length。
现在编译:
npx tsc没有任何输出就是成功。你会看到生成了一个dist目录,里面是index.js、types.js和对应的.map文件。对比一下源码和产物:类型标注全部消失了,接口和类型别名也不见了,联合类型被擦除,只剩下纯粹的 JavaScript。这就是所谓"类型擦除",TypeScript 不改变运行时行为,它只是加了一层编译期的保护壳。
执行产物:
node dist/index.js如果你已经配置了"type": "module",输出会类似:
[x] #1 装好 Node 和 VSCode #环境 [ ] #2 写出第一个接口如果想跳过编译直接用 tsx 跑源码:
npx tsx src/index.ts结果一样,但省掉了编译步骤。开发时用这条,发布前跑一次完整的 tsc。
3.3 配好断点调试,比 console.log 强十倍
新手最先学会的调试手段是console.log,这没错,但它的局限很明显:你只能在事先想到的位置打印,遇到循环、递归、异步回调的时候,打印出来的日志能淹死人。断点调试可以让你在任意位置暂停程序、查看当前所有变量的值、单步执行、观察调用栈,效率完全不是一个量级。
在项目根目录建.vscode文件夹,里面放一个launch.json:
{ "version": "0.2.0", "configurations": [ { "name": "调试当前 TS 文件", "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/tsx/dist/cli.mjs", "args": ["${file}"], "console": "integratedTerminal", "skipFiles": ["<node_internals>/**"] } ] }打开src/index.ts,在function format那一行的行号左侧点一下,会出现一个红点,这就是断点。按 F5(macOS 上是 Fn+F5 或直接点左侧调试图标),程序会启动并停在断点处。此时左侧面板会显示当前作用域下所有变量:t是什么、mark是什么、tags是什么,一目了然。顶部工具栏有继续、单步跳过、单步进入、单步跳出四个按钮,分别对应 F5、F10、F11、Shift+F11。
几个实操心得。skipFiles这个配置用来告诉调试器跳过 Node 内部文件,不加的话你按单步进入很容易掉进 Node 源码里出不来,得点几十次才能回到自己的代码。console设为integratedTerminal的好处是输出会走集成终端,支持颜色和交互;如果用默认的调试控制台,某些库输出的彩色日志会变成一堆转义字符。
还有一点:调试用的是 tsx 来跑源码,所以 source map 是由 tsx 提供的,不需要依赖 tsc 生成的 map。这两种方式不冲突,但别搞混。如果你把program指向dist/index.js,那就走 tsc 生成的 source map,前提是sourceMap: true开着,且dist已是最新。我个人更推荐调试源码这条路线,改完立刻能调,不用等编译。
3.4 用 npm scripts 把命令固化下来
每次手敲npx tsc太累,也容易敲错。在 package.json 里加一组脚本:
{ "type": "module", "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", "typecheck": "tsc --noEmit" } }tsx watch会监听文件变化并自动重启,改完代码不用手动执行,这是日常开发最常用的命令。build负责产出。start跑产物,模拟线上环境。typecheck只做类型检查不输出文件,适合放在提交前的检查流程里。
有一点要提醒:tsc --noEmit和tsc的检查强度是一样的,区别只是不写文件。我见过有人在开发时用 tsx 跑得很欢,结果 CI 上一跑 tsc 报出二十个错误,原因就是 tsx 只擦除类型不做检查。所以每天至少完整跑一次 typecheck,把这个习惯养起来,能省掉很多返工。
4. 踩坑实录:新手最容易卡住的问题清单
这一节是全文我最想让你认真看的部分。下面这些坑,基本每个我带过的人都踩过至少三个。我把它们整理成速查表,遇到报错先来这里对一遍,大概率能直接定位。
4.1 常见报错速查表
| 报错信息 | 根本原因 | 解决方式 |
|---|---|---|
Cannot find module 'xxx' or its corresponding type declarations | 缺少类型声明文件 | 装@types/xxx,或自己写.d.ts声明 |
Cannot use import statement outside a module | 模块规范不匹配 | package.json 加"type": "module",或改 tsconfig 的 module |
'xxx' is declared but its value is never read | 开了noUnusedLocals且有未使用变量 | 删掉变量,或给无用参数加下划线前缀 |
Object is possibly 'undefined' | strictNullChecks生效 | 加可选链?.或显式判空 |
File 'xxx' is not under 'rootDir' | 被编译的文件超出了 rootDir 范围 | 挪进 src,或调整 rootDir |
Type 'string' is not assignable to type 'number' | 类型不匹配 | 按提示修正,别用 as any 硬压 |
tsc 不是内部或外部命令 | 全局未装且在错误的目录执行 | 用npx tsc,或确认当前在项目根目录 |
| 中文路径下编译乱码或找不到文件 | 工具链对非 ASCII 路径支持不佳 | 项目移到纯英文路径 |
关于最后一条我想多说两句。as any是新手最容易上瘾的"解药"。报错了?加个as any,红线立刻消失。但这么做的本质是把类型系统关掉了,你付出学习成本却一点收益都没拿到。我的建议是给自己定一条规矩:只有在你确认这是第三方库的类型定义有问题时才允许用 any,并且必须写注释说明原因。其他情况下宁可多花十分钟去理解报错在说什么。
还有@types/xxx这个规律要记住:绝大多数主流 JavaScript 库的类型声明都发布在@types作用域下,由社区维护。判断一个库有没有自带类型,最直接的办法是看它有没有index.d.ts文件,或者去类型声明的检索站点搜一下包名。搜不到的话,就得自己写一个.d.ts文件声明模块,这是进阶内容,遇到再说。
4.2 路径、模块与 ESM 的几个深坑
import必须带扩展名这件事,前面提过一次,这里系统说一下。在 Node 的 ES Module 规范里,相对导入必须写完整的文件路径,包括扩展名。TypeScript 源码里你写的是./types.js,但因为编译后确实是types.js,所以运行时能对上。你写./types.ts会报错,写./types在某些配置下也会报错。
这个规则和 CommonJS 时代完全不同。CommonJS 里require("./types")是标准写法,Node 会自己尝试补全.js、.json、.node。所以很多老教程教你省略扩展名,照抄到 ESM 项目里立刻报错。判断自己的项目走哪套规范,看 package.json 有没有"type": "module"就够了。
另一个坑是__dirname和__filename。这两个变量在 CommonJS 里是内置的,但在 ES Module 里不存在。如果你要用它们来定位文件路径,得这样写:
import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);这段代码我建议直接存成片段,用到就复制。无数人在这上面卡过半小时,报错信息还是__dirname is not defined,看着像是 Node 出问题了,其实是模块规范的原因。
关于baseUrl这个选项,如果你看到别人教程里用它来配路径别名,这里要提醒一句:baseUrl已经被标记为弃用,在未来的 TypeScript 版本中会被移除。现在的推荐做法是在paths里直接用相对 tsconfig 所在目录的路径,不再依赖baseUrl:
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }不过要说清楚,这个改动只影响 TypeScript 的类型解析。运行时 Node 并不认识@/这种别名,你还需要额外的方案来处理运行时路径映射,比如打包工具或者运行时加载器。新手阶段建议先不要碰路径别名,老老实实写相对路径,等环境完全跑通、对模块解析机制有感觉了再折腾。过早引入别名,会让报错信息变得难以理解,因为编译期和运行时的解析结果不一致时的报错非常隐晦。
4.3 插件冲突与编辑器卡顿的处理
VSCode 里有一个很少人知道但非常关键的设置:内置 TypeScript 版本可以切换。打开任意.ts文件,按Ctrl+Shift+P,输入TypeScript: Select TypeScript Version,你会看到两个选项:一个是 VSCode 自带的版本,一个是工作区里node_modules安装的版本。一定要选工作区那个。
为什么?因为编辑器提示和 tsc 编译使用的必须是同一个版本。如果编辑器用内置的 5.0,项目装的是 5.4,新语法在编辑器里标红但编译能过,或者反过来,那种"编辑器报错但能跑"的诡异现象就是这么来的。团队里可以把这个选择写进.vscode/settings.json:
{ "typescript.tsdk": "node_modules/typescript/lib" }卡顿问题通常在项目变大之后出现。语言服务需要扫描所有被 include 的文件,如果 node_modules 没排除干净,扫描量会爆炸。检查一下 tsconfig 里的exclude,同时在 VSCode 设置里加一条文件监听排除:
{ "files.watcherExclude": { "**/node_modules/**": true, "**/dist/**": true } }我在一个中型项目上实测过,加上这两条排除之后,冷启动的语言服务初始化时间从十几秒降到了三秒以内,保存时的自动提示延迟也明显降低。这不是玄学,就是扫描范围小了。
5. 把环境用顺手:格式化、规范与个人配置分享
环境能跑只是起点,真正让它好用的是后面这些顺手的小配置。这一节的内容都不复杂,但每一条我都在实际项目里用了很久,属于"配一次省一年"的类型。
5.1 接入 ESLint 与 Prettier 的分工
这两个工具容易搞混,先分清职责:Prettier 管格式,ESLint 管质量。缩进几个空格、要不要分号、行宽多少,这些是格式问题,交给 Prettier;变量声明了没用、用了==而不是===、函数复杂度太高,这些是质量问题,交给 ESLint。让它们各管一摊,就不会互相打架。
安装:
npm install --save-dev eslint prettier eslint-config-prettiereslint-config-prettier这个包的作用是关掉 ESLint 里所有和格式相关的规则,避免两个工具给出矛盾的修复建议。这一步非常关键,不装的话你会遇到"保存一次代码格式被改两遍、互相来回推"的诡异现象,我当年被这个折腾了一整个下午才找到原因。
然后在项目根目录建.prettierrc:
{ "semi": true, "singleQuote": false, "printWidth": 100, "trailingComma": "es5", "tabWidth": 2 }printWidth设 100 是我个人的偏好,默认的 80 在现代宽屏上换行太频繁,读起来反而累。这个值团队内统一就行,没有绝对的对错,重要的是统一。
最后在.vscode/settings.json里打开保存自动格式化:
{ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" } }注意source.fixAll.eslint的值,新版本 VSCode 要求写"explicit"而不是true,写错了不生效也不报错,很多人卡在这里以为是插件坏了。这个细节文档里藏得比较深,我是查 issue 才找到的。
5.2 我个人在用的完整配置片段
把前面的内容合起来,我现在的.vscode/settings.json大致长这样,可以直接抄:
{ "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "files.watcherExclude": { "**/node_modules/**": true, "**/dist/**": true, "**/.git/**": true }, "files.exclude": { "**/dist": false }, "typescript.preferences.importModuleSpecifierEnding": "js", "typescript.updateImportsOnFileMove.enabled": "always" }其中typescript.preferences.importModuleSpecifierEnding设为js,是为了让编辑器自动补全导入路径时带上.js后缀,正好匹配 ESM 的要求,省得你每次手动改。updateImportsOnFileMove设为always,意思是移动或重命名文件时自动更新所有引用它的导入路径,这个功能在重构时能救命,强烈建议开。
files.exclude里我把dist设成了false,也就是不隐藏。原因是学习阶段我建议你多看看编译产物,观察类型是怎么被擦除的、interface编译后变成了什么、enum生成了多少额外代码。这种观察对建立直觉特别有帮助。等项目大了,产物多了看着烦,再改成 true 隐藏掉。
5.3 从这套最小环境往前走
环境搭完之后,往哪个方向走取决于你的目标。想写后端服务,可以看看基于 TypeScript 的服务端框架,装饰器、依赖注入那一套用起来很舒服,但要注意它对 tsconfig 有些特定要求,比如experimentalDecorators和emitDecoratorMetadata得打开。想做前端,主流框架的脚手架基本都内置了 TypeScript 模板,一条命令就生成好完整配置,你只需要看懂它生成的 tsconfig 就行,而经过今天这一遍,你已经有能力看懂了。
再给一个判断标准。如果你现在打开一个陌生项目的 tsconfig,能说出strict、target、moduleResolution这三项各是干什么的,那你已经脱离小白阶段了。这三项覆盖了 TypeScript 配置里百分之八十的实际问题。
我个人在这些年反复配置环境的过程中,最大的体会是:不要追求一次配到完美。我见过有人在项目第一天就花两天时间研究 monorepo 配置、路径别名、构建缓存,结果代码一行没写,热情先耗没了。正确的顺序是先用最小配置跑通一个能运行的脚本,然后随着遇到的真实问题逐个往上加。每加一个配置,你都知道它是为了解决什么具体问题才存在的,这样配出来的东西你才真正掌握,而不是从别人仓库里复制一堆看不懂的 JSON。环境这件事,够用就好,能跑、能调、能报错,就已经覆盖了入门阶段的全部需求。