
最近我把 TypeScript 的知识点重新梳理了一遍原因很简单项目里出的坑、同事写的代码、接口返回的类型定义都让我意识到“会写 TypeScript”和“把 TypeScript 用明白”之间有一条很深的沟。网上的 TypeScript 教程很多但要么是照着官方文档抄一遍要么是一堆面试题拼盘真正结合工程配置、实际场景和版本坑讲清楚的太少了。这篇文章不是从零教语法而是把我整理笔记时重点留下的东西拿出来聊聊类型设计思路、编码规范、tsconfig 里的弃用选项再顺着 Vue3 Three.js 机房可视化、React、QuickJS 这些真实场景展开。适合已经写过一阵子 TypeScript、正准备把项目质量往上提一档的人也适合正在准备 TypeScript 面试的人。1. 从“能用”到“会用”TypeScript 知识体系该怎么搭1.1 类型系统不是语法糖是“约束与推导”的平衡很多新手把 TypeScript 当成“加了类型的 JavaScript”觉得无非是多写几个冒号和接口。但真正用起来你会发现TypeScript 的核心不是标注而是在“约束”和“推导”之间找到平衡。类型推导是 TypeScript 帮你做的事。你不写类型它也能从初始值推出来let count 0; // count 自动是 number不需要手写 : number但推导也有边界。从一个函数返回{ status, data }如果内部逻辑复杂TypeScript 推导出来的类型往往不够精确或者会把string | null混进去。这时候就需要显式标注把“意图”告诉编译器。我常用的一个生活类比类型系统像合同签之前的条款审核。写代码时你的变量、函数参数、接口返回值都是合同的条款TypeScript 在编译阶段做审核员提前发现“这里约定了数字你怎么传了个字符串”这类问题。人的注意力是有限的靠肉眼 review 不可能每次都盯住类型但编译器可以。真正让 TypeScript 比 JavaScript 高级一档的是“类型收窄”。同一个变量在不同分支里可以被收窄成更具体的类型function handleInput(value: string | number) { if (typeof value string) { return value.toUpperCase(); } return value.toFixed(2); }这看起来简单但背后是 TypeScript 对控制流的理解。很多人在类型判断上写得很乱主要原因是不知道typeof、in、instanceof、自定义类型守卫都能触发收窄。我会在后面的实操部分再展开。知识体系不用一上来就背概念。我建议按“基础类型 → 对象类型 → 函数类型 → 泛型 → 高级工具类型 → 工程配置 → 框架集成”的顺序学每一步都在真实代码里用一遍。笔记不是抄官方文档而是记录自己踩过的坑和思考过程。1.2 编码规范从命名到严格模式一个都不能少TypeScript 编码规范是热搜词里常出现的但很多人只关注“变量用 camelCase 还是 PascalCase”。我觉得更重要的规范是让 TypeScript 的严格模式真正开启再用 ESLint 把不符合类型风格的操作拦下来。先看 tsconfig 的基础盘我建议所有新项目都开起来{ compilerOptions: { strict: true, noImplicitAny: true, noUnusedLocals: true, noUnusedParameters: true, noFallthroughCasesInSwitch: true } }strict: true是总开关它会连带开启strictNullChecks、strictFunctionTypes等一堆检查。很多从 JavaScript 转过来的人最不习惯strictNullChecks因为要处理可能为 null的场景。但正是这种“麻烦”逼着你想清楚边界。代码层面我有几个坚持了很久的规范变量命名体现类型语义isLoading、hasError、items不要用flag、data、res这种含糊名字。避免any用unknown代替any相当于关闭了该位置的类型检查等于把安全出口堵上了。unknown则强制你在使用前做收窄。接口属性不需要重复I前缀IUser这种风格在现代代码里已经过时了直接User更干净跟type一致。函数返回类型尽量显式尤其是对外暴露的 API、组件 props、工具函数返回类型写清楚改动时才不会悄悄破坏调用方。编码规范和工具链也要配合。我常用的是typescript-eslint插件推荐开启no-explicit-any、ban-ts-comment禁止随意ts-ignore、consistent-type-imports强制类型导入用import type。import type这个细节好处很明显它明确告诉你这是纯类型编译后会完全擦除不会跑到运行时里也方便打包器做 tree-shaking。2. 核心类型技巧接口、联合、泛型与实操细节2.1 interface 和 type 到底怎么选这是 TypeScript 社区最经典的问题之一。其实两者在大多数场景下可以互换但有几个关键差异interface支持声明合并。同一个名字可以声明多次自动合并。库的类型定义里经常用这个特性。type可以定义联合类型、交叉类型、条件类型、映射类型这些interface做不了。interface更擅长描述“对象的形状”type更擅长描述“类型组合”。我个人的项目约定很简单描述 props、state、接口返回、实体模型时优先用interface。做联合类型、工具类型、偏函数式组合时用type。举个例子interface ApiResponseT { code: number; data: T; message: string; } // 这个只能用 type type LoadingState idle | loading | success | error;有人会担心interface和type混用导致代码风格不一致。其实只要团队约定好了就行关键是不要同一类场景一会儿用interface一会儿用type。从心智负担的角度讲把interface当作“静态结构定义”把type当作“类型运算”项目会很清晰。2.2 泛型进阶条件类型与 infer 的实战魅力泛型不是简单地把类型当成参数。真正体现 TypeScript 能力的是条件类型和infer。条件类型的写法类似三目运算type IsArrayT T extends any[] ? true : false;infer则是“在类型匹配过程中声明一个待推断的类型变量”。最经典的例子是取函数返回类型type MyReturnTypeT T extends (...args: any[]) infer R ? R : never; type A MyReturnType() string; // string我在实际项目里经常用infer解决“从已有类型里抠出某个部分”的需求。比如后端返回一个分页结构interface PageDataT { list: T[]; total: number; page: number; } type ItemTypeOfT T extends PageDatainfer U ? U : never;这样前端拿到PageDataDevice就能用ItemTypeOfPageDataDevice推导出Device避免重复写一遍类型。条件类型还经常配合keyof使用例如写一个“只保留指定 key 的类型”的筛选器type PickByValueT, V { [K in keyof T as T[K] extends V ? K : never]: T[K]; };这种类型体操容易让人上瘾但有度。我见过团队把简单业务搞出十层泛型最后没人能维护。原则是泛型复杂度应当服务于代码复用而非展示聪明。如果一段泛型没人能读懂那它在团队协作里就是负资产。2.3 keyof / typeof / 映射类型组合起来才有力量单独记keyof和typeof很简单难点在于组合。typeof能从一个运行时的变量反推出类型这个在配合常量配置时特别好用const statusMap { online: { label: 在线, color: #52c41a }, offline: { label: 离线, color: #ff4d4f }, fault: { label: 故障, color: #f5222d }, }; type StatusKey keyof typeof statusMap; // online | offline | fault type StatusValue typeof statusMap[StatusKey]; // { label: string; color: string }这样写的好处是statusMap是唯一的“事实来源”类型推导自动跟随它变化。以后加一种状态所有用到StatusKey的地方都会同步更新不会出现“配置加了类型忘了加”的问题。映射类型最常见的场景是表单状态更新。比如写setField方法时希望key和value一一对应interface FormState { name: string; age: number; remark: string; } type FieldUpdater { [K in keyof FormState]: (value: FormState[K]) void; }; const updater: FieldUpdater { name: (v) v.toUpperCase(), age: (v) v.toFixed(0), remark: (v) v.trim(), };这里每个回调参数类型都跟 FormState 对应 key 的类型锁死。你写age: (v) v.toUpperCase()会直接报错因为v是number。这种类型安全带来的开发体验是 JavaScript 时代完全无法想象的。3. tsconfig 工程化弃用选项与迁移踩坑3.1 baseUrl 已经弃用paths 要这样写如果你最近升级 TypeScript 版本可能已经见过类似警告选项baseUrl已弃用并将停止在 TypeScript 7.0 中运行。很多老项目里都有这么一段{ compilerOptions: { baseUrl: ./, paths: { /*: [src/*] } } }以前这么写很方便/components/Button直接映射到src/components/Button。但现在 TypeScript 官方明确表示要移除baseUrl。原因是baseUrl会让路径解析规则变得隐式尤其是在大型 monorepo 和多包环境下不同 tsconfig 的 baseUrl 相互影响排查问题很头疼。好消息是从 TypeScript 4.1 开始paths本身已经可以完全脱离baseUrl使用。新的写法是{ compilerOptions: { paths: { /*: [./src/*] } } }注意paths里的路径是相对于 tsconfig 文件所在目录的。这样去掉baseUrl后路径语义更清晰也不再依赖那个弃用选项。如果你的项目还在用baseUrl迁移步骤很简单删掉baseUrl把paths里的相对路径前面补上./重新编译验证一遍即可。还有一个很容易忽略的地方baseUrl也影响非相对路径导入。比如以前能直接import { foo } from utils/helper就是因为baseUrl指定了根目录。去掉baseUrl之后这类导入会失效要改成import { foo } from ./utils/helper或通过paths显式映射。3.2 moduleResolution: node10 弃用了改用 node16 还是 bundler另一个弃用警告是moduleResolution: node10。这个名字听着奇怪其实它就是过去经典moduleResolution: node的模式。在 TypeScript 5.0 之后官方把旧值重新命名为node10并明确标记为弃用计划在未来版本移除。旧模式的问题在于它模拟的是 Node.js 早期的 CommonJS 解析规则去找node_modules、文件尝试扩展名、目录找index.ts。这种规则没有认真对待package.json里的exports字段与现代 Node.js 生态已经脱节了。现在推荐的选择有两个node16/nodenext适合纯 Node.js 服务端项目尊重package.json的exports和import/require条件严格区分 ESM 和 CJS。bundler适合用 Vite、webpack、Rollup 这类打包器的前端项目。它不强制模块扩展名容忍exports的某些省略解析方式更贴近打包器实际行为。以一个 Vite Vue3 前端项目为例推荐配置是{ compilerOptions: { module: ESNext, moduleResolution: bundler, target: ES2020 } }注意moduleResolution: bundler要求module不能是commonjs。如果你看到 TS2307 “Cannot find module” 的报错很多时候不是路径错了而是moduleResolution和module不匹配。3.3 迁移实操一次 tsconfig 升级实录我帮同事迁移过一个老项目原来的配置大概是{ compilerOptions: { target: es5, module: commonjs, moduleResolution: node, baseUrl: ./, paths: { /*: [src/*] } } }迁移后的样子{ compilerOptions: { target: ES2020, module: ESNext, moduleResolution: bundler, strict: true, paths: { /*: [./src/*] } } }迁移过程中碰到三个问题第一es5升级到ES2020后一些 Promise、Array、Map 的语法报错消失了但要注意是不是有代码依赖老环境。前端项目只要不是要兼容 IE 老版本都没问题。第二module: commonjs改成ESNext后代码里的require()全被提示。如果还有动态导入要改成标准的import()或import.meta。第三baseUrl删除后那些没有经过paths的模块相对导入全部显形。比如import { x } from src/foo原本能解析现在会报错改成./src/foo或利用paths别名。建议迁移时先跑一遍tsc --noEmit把报错清单拉出来按模块归属分批处理。不要想着一晚上改完最容易出问题的是全局替换导致导入路径语义改变。4. 场景落地Vue3 Three.js TypeScript 机房可视化项目4.1 业务模型与类型设计先有类型再有界面最近做的机房可视化项目就是典型场景基于 Vue3 Three.js TypeScript 构建一个 3D 机房图机柜、设备、网络连线都展示在三维场景里。这类项目如果不设计好类型光靠 Three.js 对象和any互相乱指后面改需求时会非常痛苦。第一步先定义业务模型不要一上来就写 Three.js 代码。机房里有几个核心实体机房 Room、机柜 Rack、设备 Device、连线 Link。基础类型如下type DeviceStatus online | offline | fault; interface Rack { id: string; name: string; position: [number, number, number]; // 机柜左上角 units: number; // 机柜总U数 width: number; depth: number; } interface Device { id: string; rackId: string; name: string; type: server | switch | storage; startU: number; // 起始U位从下往上 height: number; // 占用U数 status: DeviceStatus; } interface Room { id: string; name: string; size: [number, number, number]; racks: Rack[]; }这里有几个设计细节用type定义DeviceStatus字符串联合而不是用enum。字符串联合更符合 JavaScript 的习惯类型提示直接显示具体取值序列化也简单。用元组[number, number, number]表示三维坐标比{ x, y, z }更轻量但要注意元组长度是固定的不能随便索引越界。rackId作为外键在数据关联时比直接嵌套对象更利于数据扁平化更新。有了这些模型Three.js 场景里的每个 Mesh 都可以关联一个业务实体interface RackMesh extends THREE.Group { rackData: Rack; } interface DeviceMesh extends THREE.Group { deviceData: Device; }不过我更推荐用 Map 而不是自定义类型扩展 Three 对象const rackMap new Mapstring, { group: THREE.Group; data: Rack }(); const deviceMap new Mapstring, { mesh: THREE.Group; data: Device }();这样 Three.js 场景对象和业务数据是解耦的点击选中某个设备时直接根据deviceMap查找对应数据不用在 Mesh 上塞各种自定义属性。4.2 Three.js 中 TypeScript 的常见坑Three.js 有自己的类型但版本升级频繁和 TypeScript 的配合有挺多需要注意的地方。第一个坑是OrbitControls等扩展模块的导入。旧版教程里常见的是import { OrbitControls } from three/examples/jsm/controls/OrbitControls但新版本 Three.js 的推荐导入方式已经变成从three/addons/导入import { OrbitControls } from three/addons/controls/OrbitControls.js;类型问题常常出在three/examples和three/addons混用上。如果你用的是 Vite types/three建议统一使用three/addons并且让moduleResolution为bundler或node16否则 TS 可能找不到这些模块的声明。第二个坑是 DOM 元素可能为 null。Three.js需要挂载到一个 canvas 或 div 上Vue3 里用模板 refconst containerRef refHTMLDivElement | null(null); let renderer: THREE.WebGLRenderer | null null; function initThree() { if (!containerRef.value) return; renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight); containerRef.value.appendChild(renderer.domElement); }如果不做if (!containerRef.value) returnTypeScript 会一直提示“containerRef.value is possibly null”。这是strictNullChecks最常见的报错解法是类型守卫而不是非空断言!。后期代码里用到的变量同样要守卫。第三个坑是动画循环里的事件类型。用window.addEventListener(resize, onResize)没问题但如果监听鼠标点击事件对象类型要写清楚function onCanvasClick(event: MouseEvent) { const rect renderer.domElement.getBoundingClientRect(); const mouse new THREE.Vector2( ((event.clientX - rect.left) / rect.width) * 2 - 1, -((event.clientY - rect.top) / rect.height) * 2 1 ); raycaster.setFromCamera(mouse, camera); }这里MouseEvent是 DOM 的类型不是 Three.js 的。如果你在 Vue 模板里绑定click事件对象其实是MouseEvent类型写好之后能直接拿到clientX很省心。4.3 在 Vue3 中封装 Three 逻辑一个 useThree 组合式函数为了让 Three.js 代码不跟 Vue 组件纠缠在一起我会把初始化、渲染循环、销毁都封装成组合式函数。大致结构如下export function useThree(containerRef: RefHTMLDivElement | null) { let renderer: THREE.WebGLRenderer; let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; let animationId: number; function init() { if (!containerRef.value) return; scene new THREE.Scene(); camera new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); renderer new THREE.WebGLRenderer({ antialias: true }); // ... } function animate() { animationId requestAnimationFrame(animate); renderer.render(scene, camera); } function dispose() { cancelAnimationFrame(animationId); renderer.dispose(); // 清理场景中的 Mesh、Texture } return { init, animate, dispose, scene, camera }; }在组件里使用const containerRef refHTMLDivElement | null(null); const three useThree(containerRef); onMounted(() { three.init(); three.animate(); }); onBeforeUnmount(() { three.dispose(); });这里函数返回类型值得注意。useThree里的scene,camera,renderer如果初始化前就 return会有“未赋值的变量”报错。用let并加!声明或者把 init 的逻辑做进一个类里。我实际更推荐写一个不依赖 ref 的ThreeScene类再在useThree里做 Vue 桥接职责更清楚。5. 生态问答React、QuickJS 与 Playground5.1 React TypeScript几个很实用的经验React 项目用 TypeScript最常见的问题是组件 props 类型怎么定义。我现在的习惯是直接用一个interface定义 props不纠结React.FC还是普通函数组件。interface ButtonProps { label: string; onClick?: (e: React.MouseEventHTMLButtonElement) void; disabled?: boolean; } export function Button({ label, onClick, disabled false }: ButtonProps) { return ( button onClick{onClick} disabled{disabled} {label} /button ); }有几个细节useState如果初始值是空数组要写类型参数。useState([])会被推导成never[]后面push任何对象都报错所以写useStateDevice[]([])。useRefHTMLDivElement(null)类型一般要写HTMLDivElement | null因为初始值null否则访问.current可能报错。事件处理函数里的e.target类型不定要收窄。比如在 input 上最好用React.ChangeEventHTMLInputElement而不是any。子组件接收 children不要写props.children: React.ReactNode直接把Props扩展React.PropsWithChildren代码更简洁。React TypeScript 还有一个大坑是第三方库没有类型声明。一般的解决方式是写.d.tsdeclare module some-chart-lib { export interface ChartOptions { title?: string; data: number[] } export function render(el: HTMLElement, options: ChartOptions): void; }不要一遇到没类型的库就as any写一个最小声明文件既能让编译器闭嘴也能给队友留下线索。5.2 QuickJS 支持 TypeScript 吗搜索热词里有个很有意思的问题QuickJS 支持 TypeScript 吗很多人可能把它跟 JavaScript 混了。QuickJS 本身是一个轻量级 JavaScript 引擎用在嵌入式系统、小程序容器、安全沙箱里它运行的是 JavaScript不是 TypeScript。TypeScript 是 JavaScript 的超集浏览器和引擎不直接运行 TypeScript。所以 QuickJS 不支持也不应该支持直接执行.ts文件。正确的做法是先用 TypeScript 编译器把代码编译成目标版本的 JavaScript再交给 QuickJS 执行。比如你用 TypeScript 写一段业务逻辑function add(a: number, b: number): number { return a b; }先用tsc编译成 ES2019 或更低的 JavaScripttsc --target ES2019 --module ES2020 --outDir dist然后把dist/add.js放到 QuickJS 沙箱里执行。要注意的是如果代码里有import/exportQuickJS 可能不支持 ESM 加载需要编译成 CommonJStsc --target ES2019 --module commonjs --outDir dist嵌入式场景里模块解析和动态导入都要提前考虑。我的建议是把 QuickJS 当作“只接受编译后 JS”的运行时TypeScript 编译阶段就做掉所有类型检查和降级这样两边都清爽。5.3 TypeScript Playground高级类型调试利器TypeScript Playground官方在线演练场是我调试复杂类型的首选工具。它不需要安装任何环境打开即用而且能直观看到类型推导的结果。Playground 的几个核心用法切换 TypeScript 版本。复现“只在某个新版本才会出现的类型报错”时很有用比如验证satisfies是否可用。左侧写 TypeScript右侧实时展示编译后的 JavaScript。能看清类型擦除后的代码理解interface怎么变成空enum变成什么。点击右侧类型展开按钮能看到复杂工具类型的最终结果比如AwaitedT、ReturnTypeT。可以把一段代码保存成分享链接方便发到群里跟人讨论。我调试类型体操时的常规流程是先在 Playground 写一个最小复现比如手写MyOmit加上几个测试用例看类型是否符合预期。确认没问题后再搬进项目这样可以避免在大型项目里反复tsc编译浪费时间。如果你不知道怎么用 Playground 调试泛型可以试试把一个复杂类型鼠标悬浮到调用处看 TypeScript 给出的提示。这个习惯比看文档更有帮助能让你直观理解类型推断的方向。5.4 TypeScript 面试到底在考什么TypeScript 面试题的热度常年不减是因为面试官真正想考察的不只是语法记忆而是你对类型安全的理解和工程化能力。常见的考点可以分为四层第一层是基础类型题type和interface的区别、enum是否应该用、unknownvsany。这一层考察你是不是真的写过代码。第二层是类型工具题手写PartialT、RequiredT、ReadonlyT、PickT, K、ExcludeT, U。别小看这些很多人背过内置类型但写不出映射类型实现。第三层是综合推断题给一个复杂对象让你从它推导出某几个字段的类型或者给一个函数泛型让你推断返回值类型。这里keyof、typeof、infer、satisfies是重点。第四层是工程题怎么配置 tsconfig、说什么情况下要写.d.ts、怎么处理第三方库的类型缺失、如何把 JavaScript 老项目渐进式迁移到 TypeScript。这些题目直接对应生产环境里的真实问题。我的面试准备建议是不要背答案手动在编辑器和 Playground 里把每个类型工具实现一遍。比如自己实现AwaitedT你会回到infer的递归这时候才算真正理解了它。面试官问到的时候你能说出实现原理比背一个结果强太多。6. 踩坑实录与速查表6.1 常见报错排查表我在整理笔记时把工作中遇到的高频报错汇总成了一表方便随时查阅。报错信息常见原因解决思路Cannot find module ./xxx or its corresponding type declarations路径大小写不一致 / 缺少 .d.ts / moduleResolution 不匹配先用相对路径验证再看模块声明和 tsconfig 的 moduleResolutionOption baseUrl is deprecated and will stop functioning使用了旧版路径解析删除 baseUrlpaths 路径改为相对 tsconfig 写法Object is possibly nullstrictNullChecks 开启且变量可能为空用类型守卫处理不用非空断言硬扛Argument of type number is not assignable to parameter of type string类型不匹配检查数据来源用收窄或显式转换xxx is declared but its value is never read开启了 noUnusedLocals删除未使用的变量或确认命名导出没有引用Cannot find type definition file for node缺少 types/node安装 types/node 到 devDependenciesProperty x does not exist on type Y类型定义不完整检查接口定义或通过声明合并补全Overload resolution didnt pick any overload传参不符合函数重载看看重载签名用符合签名的方式调用satisfies is only supported in later versionsTS 版本低升级 TypeScript或改用普通标注表格里的每一行都是实际能解决问题的不是理论。遇到报错先看信息里的位置和类型名再对照表格找思路通常能在五分钟内定位。6.2 我总结的“早知道就好了”的五条经验第一条严格模式越早开越好。新项目一开始就要把strict: true打开不要等项目写了一千行再开那时候报错会多到让你想重写。严格模式虽然严格但它会把不稳定因素提前暴露出来长远看省太多时间了。第二条不要用any解决类型报错。任何用any绕过去的问题都会在三个月后变成另一个人的不可解 bug。如果类型太复杂先用unknown收住再逐步收窄实在要绕过也要加注释说明原因和后续计划。第三条少用enum多用字符串联合。这个观点可能会引发争议但我在业务代码里真的很少碰 enum。字符串联合类型有可读性好、天然支持类型收窄、不会被编译成额外运行时代码等优势。遇到需要“枚举值绑定描述信息”的时候用as const对象更合适。第四条类型声明文件不要随便“任何化”。第三方库没类型时自己写.d.ts是正道。写一个最小声明文件往往只要十几行但能解决后续所有调用处的类型报错。别把没类型的库全推给declare module xxx就完事至少把暴露的函数签名写出来。第五条学会用satisfies代替直接标注。satisfies可以同时做到“保留字面量精确类型”和“检查是否满足接口”。这是 TypeScript 4.9 引入的能力非常实用。type RouteConfig { path: string; component: string }; const routes { home: { path: /, component: Home }, about: { path: /about, component: About }, } satisfies Recordstring, RouteConfig; // routes 里的每个值还是字面量类型但整体又符合 RouteConfig用普通const routes: Recordstring, RouteConfig会丢掉字面量类型用satisfies两者兼顾。6.3 学习路线与自测建议如果你现在正准备系统学 TypeScript我的建议是别一上来就啃类型体操。按照下面的路线走会更顺先过一遍基础语法原始类型、数组、元组、接口、泛型、联合类型把tsc编译流程跑通。熟练使用类型工具Partial、Required、Pick、Omit、Record、ReturnType理解它们的实现。学习 tsconfig 的核心配置target、module、moduleResolution、strict、paths、types搞懂每个选项的影响。在真实项目里练手Vue3 组合式函数、React 组件、Node 服务都能让类型设计和业务结合。适度挑战复杂类型看几个源码级别的类型工具比如Awaited、DeepReadonly能写能测就行。自测时可以问自己几个问题如果需要把一个问题抛给 ChatBot你能用准确的 TypeScript 术语描述吗你能把一个接口返回的 JSON 结构快速用 interface 定义出来吗你迁移 tsconfig 的弃用选项时能说出为什么吗如果这些问题都能回答说明你已经不是“会用 TypeScript”而是“能掌控类型安全”了。我在整理这份笔记时最大的感受是TypeScript 的知识点不是靠背的是靠一次次报错、一行行类型推导堆出来的。你现在看到的所有“经验”都是我当初踩过坑之后总结出来的教训。把这些内容放进自己的项目里跑一遍再回来看会比单纯收藏一份笔记有价值得多。