简介:这份资源面向希望在 Visual Studio Code 中高效调试 TypeScript 的开发者,尤其适合刚接触 TS 调试配置、需要打通断点与源码映射环节的初中级前端或 Node.js 工程师。内容围绕 tsconfig.json 编译选项、.vscode/launch.json 调试配置、断点设置与源码映射等核心环节展开,并延伸至 Debugger for Chrome 的前端调试与 watch、Call Stack、Scope 等进阶技巧。压缩包共 10 个文件,以 5 个 json 配置、2 个 ts 示例源码为主,另含 readme 说明、LICENSE 与 .gitignore,整体约 5KB,结构轻量,可直接作为调试环境模板参考。目前已有 1979 人学习下载。通过这份示例工程,读者能快速理解 sourceMap 与 outFiles 的对应关系,掌握本地与浏览器两种调试链路的配置差异,并借助示例代码验证断点、变量监视与调用栈分析流程,减少自行摸索配置的时间成本。
1. 调试 TypeScript 时,为什么你的断点总是不生效
如果你写过 TypeScript,大概率经历过这种场景:在.ts文件里点了个红点,按下 F5,程序跑起来了,断点却变成了空心圆圈,旁边还飘着一句 “Breakpoint set but not yet bound”。你以为是编辑器抽风,重启一遍,还是老样子。问题不在编辑器,在于 TypeScript 不能直接运行,它得先编译成 JavaScript,而调试器默认只认编译后的产物。
这个资源包解决的正是这件事:在 VS Code 里对 TypeScript 做源码级调试,让断点打在.ts上、单步走的是原始代码、鼠标悬停看到的是原始变量名。它适合三类人:刚从 JavaScript 转过来、对 sourcemap 一知半解的开发者;手里有 Node 后端或前端项目、想摆脱console.log的从业者;以及需要调试编译型语言、对 launch.json 配置总是配不利索的工程师。核心机制就一个词——sourcemap,它是连接编译产物和源码的桥梁,桥没搭好,断点自然落空。
2. 先搞懂 sourcemap 和 launch.json:断点能落地的两个前提
2.1 sourcemap 是怎么把 .js 映射回 .ts 的
TypeScript 编译后生成.js文件,同时可以生成一个.js.map文件,这就是 sourcemap。它内部是一段 JSON,核心字段包括version、sources、mappings、names。其中mappings是一串用 Base64 VLQ 编码的字符串,记录了编译产物每一个位置对应源码的哪一行哪一列。调试器读取这个映射表,才能在你于.ts文件某行下断点时,换算成.js文件里的实际位置去拦截执行。
要让 sourcemap 生效,tsconfig.json里必须打开两个开关:
{ "compilerOptions": { "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "target": "ES2020", "module": "commonjs" }, "include": ["src/**/*.ts"] }sourceMap: true负责生成.map文件;outDir和rootDir决定了编译产物的目录结构和源码的根目录,这两个路径如果和调试配置对不上,sourcemap 里的相对路径就会错位,断点照样不生效。target和module影响编译后的语法形态,Node 环境下commonjs是最稳的选择,用ESNext有时会让调试器解析模块时多绕一层。
编译一次看看产物:
npx tsc --project tsconfig.json执行后dist目录下应该出现index.js和index.js.map。打开.map文件确认sources字段里是../src/index.ts这样的相对路径,而不是绝对路径或空数组。这一步是后面所有调试配置的地基,地基歪了,launch.json 怎么写都白搭。
2.2 launch.json 里三个字段决定调试成败
VS Code 的调试行为由.vscode/launch.json驱动。针对 TypeScript 的 Node 调试,最常见的配置长这样:
{ "version": "0.2.0", "configurations": [ { "name": "Debug TS Node", "type": "node", "request": "launch", "program": "${workspaceFolder}/src/index.ts", "preLaunchTask": "tsc: build", "outFiles": ["${workspaceFolder}/dist/**/*.js"], "sourceMaps": true, "console": "integratedTerminal", "skipFiles": ["<node_internals>/**"] } ] }program指向的是源码入口.ts文件,不是编译后的.js。这是很多人第一次配就翻车的地方——填了dist/index.js,结果断点全落在编译产物上,单步走的是转译后的代码。outFiles告诉调试器去哪里找 sourcemap 文件,通配符要能覆盖到所有编译产物。preLaunchTask绑定一个 VS Code task,在启动调试前自动跑一次编译,避免你改了代码忘了编译、调试的是旧版本。
对应的tasks.json:
{ "version": "2.0.0", "tasks": [ { "label": "tsc: build", "type": "shell", "command": "npx tsc --project tsconfig.json", "group": "build", "problemMatcher": ["$tsc"] } ] }problemMatcher设为$tsc后,编译报错会直接显示在 VS Code 的问题面板里,不用切到终端看输出。group设为build让它成为默认构建任务,preLaunchTask里写的 label 必须和这里完全一致,大小写都不能差。
2.3 用 ts-node 跳过编译步骤的快捷路径
如果你不想每次调试前都等编译,可以用ts-node直接在内存里转译并运行。安装:
npm install --save-dev ts-node typescript然后把 launch.json 改成:
{ "name": "Debug TS with ts-node", "type": "node", "request": "launch", "runtimeArgs": ["-r", "ts-node/register"], "args": ["${workspaceFolder}/src/index.ts"], "sourceMaps": true, "console": "integratedTerminal", "skipFiles": ["<node_internals>/**"] }runtimeArgs里的-r ts-node/register让 Node 在启动时注册 ts-node 的 require 钩子,遇到.ts文件自动转译。args传入口文件路径。这种方式省掉了outFiles和preLaunchTask,配置更短,但代价是启动时多一层转译开销,大型项目首次启动会慢几秒。另外 ts-node 默认用tsconfig.json里的编译选项,如果项目里module设成了ESNext,Node 可能报模块解析错误,临时在 tsconfig 里加一个"ts-node": { "compilerOptions": { "module": "commonjs" } }覆盖即可。
3. 从零跑通一次调试:断点、单步、变量监视的完整操作
3.1 建一个最小可调试项目
先准备一个干净的目录,初始化并安装依赖:
mkdir ts-debug-demo && cd ts-debug-demo npm init -y npm install --save-dev typescript ts-node @types/node npx tsc --inittsc --init会生成一份带注释的tsconfig.json,把里面几个关键项改成:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "sourceMap": true, "strict": true, "esModuleInterop": true }, "include": ["src/**/*.ts"] }strict: true打开严格模式,调试时类型错误会提前暴露,不至于运行到一半才发现变量是undefined。esModuleInterop让import语法和 CommonJS 模块兼容,Node 环境下基本必开。
写一个带逻辑分支的入口文件src/index.ts:
interface User { id: number; name: string; tags: string[]; } function filterUsers(users: User[], tag: string): User[] { const result: User[] = []; for (const user of users) { if (user.tags.includes(tag)) { result.push(user); } } return result; } const users: User[] = [ { id: 1, name: "alpha", tags: ["admin", "dev"] }, { id: 2, name: "beta", tags: ["dev"] }, { id: 3, name: "gamma", tags: ["ops"] }, ]; const filtered = filterUsers(users, "dev"); console.log("filtered count:", filtered.length);这段代码有循环、有分支、有函数调用,适合用来验证断点是否落在正确位置、单步是否按源码行推进。
3.2 配置 launch.json 并验证断点绑定
在项目根目录建.vscode/launch.json:
{ "version": "0.2.0", "configurations": [ { "name": "Debug TS Node", "type": "node", "request": "launch", "program": "${workspaceFolder}/src/index.ts", "preLaunchTask": "tsc: build", "outFiles": ["${workspaceFolder}/dist/**/*.js"], "sourceMaps": true, "console": "integratedTerminal", "skipFiles": ["<node_internals>/**"] } ] }再建.vscode/tasks.json:
{ "version": "2.0.0", "tasks": [ { "label": "tsc: build", "type": "shell", "command": "npx tsc --project tsconfig.json", "group": "build", "problemMatcher": ["$tsc"] } ] }在filterUsers函数里result.push(user)那一行左侧点一下,出现实心红点。按 F5 启动调试。如果红点变成空心灰色,把鼠标悬上去看提示:提示 “Breakpoint set but not yet bound” 说明 sourcemap 没被正确加载,检查outFiles路径是否匹配实际编译产物位置;提示 “Unverified breakpoint” 通常是program指向了.js文件而非.ts。
正常绑定时,程序会停在断点处,左侧变量面板能看到user、result、users、tag的当前值。把鼠标悬停在user.tags上,应该显示["admin", "dev"]这样的原始数组,而不是编译后的_a或_b之类的临时变量名。这说明 sourcemap 的names字段在起作用。
3.3 单步、条件断点和监视表达式的实际用法
断点命中后,调试工具栏从左到右依次是继续、单步跳过、单步进入、单步跳出、重启、停止。单步跳过(F10)执行当前行但不进入函数内部;单步进入(F11)会钻进filterUsers函数体;单步跳出(Shift+F11)从当前函数返回到调用处。调试循环时,如果数组有几百条数据,逐条单步不现实,这时候用条件断点:右键红点,选择 “Edit Breakpoint”,输入表达式user.id === 2,只有 id 为 2 的那次循环才会停下来。
监视表达式在左侧 “WATCH” 面板添加,比如输入filtered.length,单步过程中它的值会实时刷新。也可以在 “DEBUG CONSOLE” 里直接敲users.map(u => u.name),调试器会在当前执行上下文中求值并返回结果。这个功能在排查复杂数据结构时比反复加console.log高效得多,而且不用改代码、不用重新编译。
一个容易忽略的细节:console: "integratedTerminal"让程序的console.log输出到 VS Code 集成终端,而不是调试控制台。这样输出和普通终端运行一致,颜色和格式都不会乱。如果设成internalConsole,某些依赖终端交互的库会报错。
4. 避坑排查:断点不生效、路径错位、编译不同步的五个血泪经验
4.1 断点空心、提示 not bound
现象:.ts文件里断点是灰色空心圆,悬停提示 “Breakpoint set but not yet bound”。
原因:调试器启动时没有找到对应的 sourcemap,或者 sourcemap 里的sources路径和当前工作区文件路径对不上。
解决:先确认tsconfig.json里sourceMap: true且outDir下有.js.map文件。然后检查 launch.json 的outFiles是否指向了正确的编译产物目录。如果项目用了 monorepo 或路径别名,sourcemap 里的相对路径可能基于rootDir计算,和 VS Code 工作区根目录不一致,需要在 launch.json 里加"sourceMapPathOverrides"做映射,例如{"../src/*": "${workspaceFolder}/src/*"}。
4.2 断点停在了编译后的 .js 文件里
现象:断点确实停了,但打开的是dist/index.js,单步走的是转译后的代码,变量名变成了_a、_b。
原因:launch.json 的program字段填成了编译产物路径,调试器直接把.js当入口启动,sourcemap 虽然加载了但断点绑定优先级落在了产物上。
解决:把program改回${workspaceFolder}/src/index.ts。如果用的是 ts-node 方案,确认runtimeArgs里的-r ts-node/register没有被拼错,且args指向的是.ts入口。
4.3 改了代码,调试跑的还是旧逻辑
现象:明明改了.ts文件里的判断条件,重新 F5 后行为没变。
原因:preLaunchTask没有配置,或者配置了但 task 的 label 和 launch.json 里写的不一致,编译步骤根本没执行。
解决:确认.vscode/tasks.json存在且label与preLaunchTask完全一致。可以在终端手动跑一次npx tsc --project tsconfig.json,看dist下文件的修改时间是否更新。如果用了增量编译"incremental": true,偶尔会出现缓存不一致,删掉tsconfig.tsbuildinfo再编译一次。
4.4 ts-node 报 “Unknown file extension .ts”
现象:用 ts-node 方案启动调试,终端报TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"。
原因:Node 版本较高时,ESM 模块加载器对.ts扩展名不识别,而 ts-node 的 register 钩子默认走 CommonJS 路径,两者冲突。
解决:在tsconfig.json里显式给 ts-node 指定 CommonJS 模块:
{ "ts-node": { "compilerOptions": { "module": "commonjs" } } }或者在 launch.json 的runtimeArgs里换成["-r", "ts-node/register/transpile-only"],跳过类型检查只做转译,启动更快,但类型错误不会在调试时暴露。
4.5 变量面板显示的值和预期不符
现象:断点停在某行,变量面板里某个对象显示为{...}展开后字段对不上,或者字符串被截断。
原因:VS Code 调试器对大型对象默认做惰性求值,展开时才去读取属性,如果对象在断点命中后被后续代码修改,展开时看到的是修改后的值。
解决:在 WATCH 面板里提前把关键表达式加进去,比如JSON.stringify(user),这样在断点命中瞬间就会求值并冻结结果。或者在 DEBUG CONSOLE 里用copy(user)把当前值复制到剪贴板,避免后续修改干扰。这个坑在调试异步代码时尤其常见,因为await之后的变量状态和断点处可能已经不同。
5. 进阶技巧:用调试器替代 console.log 的几个硬核习惯
调试 TypeScript 项目时,我逐渐把console.log换成了几个固定动作。第一个是日志断点:右键断点选择 “Add Logpoint”,输入user.id = {user.id}, tags = {user.tags},程序不会停下来,但每次命中都会在调试控制台输出一行。这比在代码里插console.log干净得多,调试完直接删断点,不用改源码、不用重新编译。对于循环里的高频日志,Logpoint 的性能开销也比断点暂停小。
第二个是异常断点:在调试面板的 “BREAKPOINTS” 区域勾选 “Caught Exceptions” 或 “Uncaught Exceptions”。TypeScript 编译后的代码抛出异常时,调试器会直接停在抛出位置,而不是等到上层try/catch吞掉后才在终端看到一行模糊的堆栈。排查undefined is not a function这类错误时,这个功能能省掉大量回溯时间。
第三个是调试时修改变量值:断点命中后,在变量面板双击某个变量的值,直接改成新值,然后继续执行。这在验证边界条件时特别有用——不用改代码重新跑,当场把user.id从 1 改成 999,看下游逻辑怎么处理。改完的值只在当前调试会话生效,不会污染源码。
第四个是多目标调试:如果项目是前后端分离的,前端浏览器调试和后端 Node 调试可以放在同一个 launch.json 里,用 “Compounds” 组合起来,一次 F5 同时启动两端,断点各自独立。配置方式是在 launch.json 顶层加:
{ "compounds": [ { "name": "Full Stack", "configurations": ["Debug TS Node", "Debug Chrome"] } ] }configurations里引用的名字必须是当前 launch.json 里已定义的配置名。启动后调试工具栏会出现一个下拉框,可以在不同目标之间切换。
最后说一个我踩过的坑:有段时间断点总是随机失效,排查了半天才发现是skipFiles里漏了**/node_modules/**,调试器钻进依赖包的编译产物里,sourcemap 对不上导致整个调试会话的断点绑定状态被污染。从那以后我每次新建 launch.json,skipFiles都强制写全<node_internals>/**和${workspaceFolder}/node_modules/**,再也没出现过断点玄学失效。希望帮到你。
本文还有配套的精品资源,点击获取