尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript转TypeScript速查:从迁移成本、tsconfig配置到实战落地全解析

JavaScript转TypeScript速查:从迁移成本、tsconfig配置到实战落地全解析 JavaScript转TypeScript速查表从零迁移到落地一次讲透先说明白我在写什么这是一份给“手里全是JS老项目、被同事问过八百遍『要不要上TS』”的前端开发者的速查笔记。我用一个实际迁移过的用户管理后台为例把JavaScript到TypeScript的转换路径、类型写法、配置文件踩坑、运行时报错和迁移顺序全部过一遍保证你照着做就能把项目平稳搬过去。1. 迁移前先想明白为什么值得转以及转的成本在哪很多团队对TypeScript的第一反应是“多写一堆类型标注浪费时间”。这个想法对也不对。对的部分是如果你只处理三十行的小工具函数TS确实有点多余光配环境的时间都够把代码写完两遍不对的部分是只要项目超过两千行、有超过两个人在维护类型系统带来的收益就会几何级增长。我自己的实际感受是TS最大的价值不是“写完代码后能检查错误”而是“写代码的时候IDE能给你提示”。这一步体验差异用过就回不去。1.1 转TS前必须接受的三件事第一TS不是魔法它只在编译期做检查所有类型在运行时都被擦除。所以“我用TS写了代码就不会有运行时错误”是错觉TS抓的是类型层面的错误比如把字符串当数字传给函数、漏掉对象里的必填字段、改了接口定义但没改调用方。第二任何JS代码都是合法的TS代码这意味着你可以零成本起步不用一次性重写所有文件TS官方和社区都在推“渐进迁移”。第三转TS的初期一定会有大量报错这不是你写错了而是类型检查把以前隐藏的问题全部暴露出来了这个阶段熬过去后面就顺了。1.2 明确收益才能坚持到迁移完成我经历过一次真实迁移项目是公司的营销活动配置后台约两万行原生JavaScript三个前端维护没有任何测试。迁移到TS后最大的变化不是线上bug变少了——那个本来就少——而是每次改代码时IDE能直接告诉我哪些页面调用了这个函数、传参格式对不对、改了返回值哪些地方要跟着改。以前用JS的时候改一个函数签名全项目搜索调用处全靠肉眼核对。转TS之后编辑器直接把所有调用方列出来类型不对当场标红。对团队协作的价值更明显。新同事接手项目JS时代靠文档和“人肉讲解”TS时代直接把类型定义当文档读。任何一个人都能从.d.ts或类型声明中看懂数据的形状、函数的约束。我做了个小统计迁移后code review里关于“这个参数是什么格式”的问题下降了八成。1.3 迁移方式和成本评估迁移有两条路一条是重度重构把整个项目的类型全写好适合项目不大、业务不复杂的场景另一条是渐进式先把构建工具切到TS编译器文件后缀从.js改成.ts但类型先用any兜底让项目能跑起来再逐模块细化类型。我强烈推荐第二条路因为业务不等人你不可能停线一周专门搞技术升级。渐进迁移的节奏是“跑通为先、类型跟进、周末收割”具体做法在后面第5节细说。2. 类型语法速查从JS思维切换到TS思维的关键差异如果说这一节只有一个核心那就是TS不是“给变量写注释”而是“给数据的形状做约束”。JS的变量像是一个万能容器什么东西都能装TS像是给容器贴了标签规定只能装什么。理解了这个比喻TS的大部分语法都顺理成章。2.1 基础类型从偷懒到严谨的过渡JS里你写let count 0后来往里面塞了字符串运行时不报错等用到count.toFixed(2)时才崩。TS里你写let count: number 0后面再赋值字符串编辑器当场标红。这是最基础也最重要的差异。日常开发中基础类型的标注并不复杂值得记住的几个写法// 普通变量 let count: number 0; let userName: string lily; const isDone: boolean false; // 数组有两种等价写法 let list1: number[] [1, 2, 3]; let list2: Arraynumber [1, 2, 3]; // 对象类型推荐用接口或类型别名 interface User { id: number; name: string; age?: number; // 可选属性 readonly createdAt: Date; // 只读属性 } // 联合类型变量可能是其中一种 type Id string | number; let userId: Id abc123; userId 123; // 合法 // 字面量类型变量的取值被钉死 type Status pending | success | failed; let orderStatus: Status success;有个容易忽略的点JS里的null和undefined在TS里是独立类型默认情况下所有类型都不包含它们。如果某个变量可能为空显式声明联合类型let data: string | null null; let config: { api: string } | undefined undefined;2.2 复杂类型interface、type、泛型和工具类型写业务代码时高频出现的是接口和类型别名。很多人纠结interface和type该用哪个我的经验是展示数据的结构用interface组合数据的形状用type。事实上两者在多数场景可以互换但有一个重要区别——interface支持声明合并同名接口会自动合并属性type则不行。这个东西本身很简单你不需要追求深刻。具体到业务里大概率你是跟着团队现有风格走。泛型是另一个让新手觉得难、但项目越写越离不开的东西。它的本质是“类型参数化”——写代码时不知道具体数据类型用一个T占位调用时传入真实类型。举个例子// 一个通用的请求封装 async function requestT(url: string): PromiseT { const res await fetch(url); return res.json() as PromiseT; } interface User { name: string; age: number; } // 运行时才确定真实类型 const user await requestUser(/api/user/1);这里的T让一个函数可以被所有接口复用返回类型由调用方自己指定。这种写法在封装的工具函数里很常见比如localStorage的读写封装、事件处理器的封装。TS内置的工具类型也很重要可以减少大量重复代码。写业务代码时常用的几个interface User { id: number; name: string; email: string; phone: string; } // Partial所有属性变为可选 function updateUser(id: number, changes: PartialUser) { // 可以只传其中几个字段 } updateUser(1, { name: newName }); // Pick从接口中挑出部分字段 type UserContact PickUser, email | phone; // Omit排除部分字段 type UserWithoutId OmitUser, id; // Record快速创建键值对结构 type UserMap Recordstring, User;2.3 类型收窄让TS在分支里更聪明TS编译器有“类型收窄”机制——在条件判断中类型会自动缩减到更具体的范围。举个例子function formatId(id: string | number) { // 在这个分支里id 被收窄为 string if (typeof id string) { return id.toUpperCase(); } // 在这里id 被收窄为 number return id.toFixed(2); }业务里最常见的收窄场景是处理接口返回的数据后端返回的可能是对象也可能是错误信息或者某个字段可能是空值。用if判断配合收窄比用as硬转安全得多。我自己踩过的最多坑就是“用as跳过类型检查”结果运行时还是崩。类型断言as是最后手段能不用就不用用的时候想清楚“这个值为什么一定是这个类型”。3. 工程配置实战tsconfig.json的核心选项与2025年新变化如果说类型语法是TS的“语言能力”那tsconfig.json就是TS的“运行规则”。很多初学者打开一个TS项目第一眼看到配置文件里几十个选项直接劝退。说实话不需要全部理解但有几个选项直接决定了你的项目能不能编译、any多不多、报错信息可不可读。还有一个2025年前后的重要变化TypeScript 7.0将移除两个长期使用的配置项——baseUrl和moduleResolution: node10这个话题在开发者社区已经讨论了很久。对老项目迁移来说这个变化必须提前适应不然等TS版本升级到7.0你的构建会直接挂掉。3.1 一份可用的tsconfig.json参考下面这份配置是我从一个实际迁移的Vue3项目中精简出来的可以直接作为起步模板{ compilerOptions: { target: ES2020, module: ESNext, moduleResolution: bundler, strict: true, jsx: preserve, sourceMap: true, esModuleInterop: true, allowSyntheticDefaultImports: true, resolveJsonModule: true, isolatedModules: true, skipLibCheck: true, useDefineForClassFields: true, lib: [ES2020, DOM, DOM.Iterable], types: [vite/client], noEmit: true, paths: { /*: [./src/*] }, verbatimModuleSyntax: true }, include: [src/**/*.ts, src/**/*.d.ts, src/**/*.tsx, src/**/*.vue], exclude: [node_modules, dist] }重点说几个必须改的选项。strict必须开这个选项相当于把所有严格检查全部打开包括noImplicitAny、strictNullChecks、noUnusedLocals这些子项。很多初学TS的人觉得strict太烦总是报错于是把它关掉这等于放弃了TS的核心价值。我的建议是哪怕先从strict: false起步项目稳定后也必须打开。moduleResolution要设成bundler这是对应Vite、webpack这类打包器的配置方式。老项目里大量使用moduleResolution: node10这个值在TS 7.0会被移除新项目不要再写。3.2 弃用项说明baseUrl和moduleResolutionnode10先说baseUrl的问题。它的本意是设置非相对路径导入的基准目录老项目里常见写法是baseUrl: ./src然后导入时写import utils from utils/helper。但在TS 5.0引入paths可以不依赖baseUrl独立解析之后baseUrl就变得多余了。TS团队在5.0版本就给出了弃用警告计划在7.0彻底移除。现在建新项目或做迁移时别再依赖这个字段路径别名干脆改成paths配合相对根目录的写法{ compilerOptions: { paths: { /*: [./src/*] } } }导入时写import utils from /utils/helper这样即使没有baseUrl也能正常工作。moduleResolution: node10也同理。它对应的是Node.js老旧的CommonJS模块解析方式在新版TypeScript中已经被node16、nodenext和bundler取代。旧值继续在TS 7.0到期退役。迁移项目时直接改成moduleResolution: bundler可以兼容大部分前端工程。3.3 千万别忽略的配置allowJs和checkJs渐进迁移一个老JS项目时allowJs和checkJs是救命的配置。设置allowJs: true后TS编译器允许混用.js和.ts文件JS文件不会被报错阻断。配合checkJs: true编译器还会给JS文件做类型检查但用JSDoc注释可以逐步添加类型。我的迁移策略是第一步只加allowJs让项目能以TS方式启动第二步对部分JS文件加JSDoc类型注释打开checkJs试水第三步再把JS文件重命名成.ts把JSDoc转换成正式的TS语法。有个小细节Vite项目里要确保vite.config.ts中配置的解析逻辑和tsconfig里的paths一致否则IDE里路径智能提示正常但打包时别名解析失败。这类问题排查起来比写类型还消耗时间我下面专门列一节讲。4. 常见报错与运行时陷阱代码能编译不等于万事大吉从JS转TS后很多人遇到的第一波挫败感不是来自语法而是来自“我明明照着例子写了为什么还是报错”这里把实际迁移过程中最常见的几类问题整理出来都是我自己或者团队同事踩过的。4.1 编译期报错的四类高频原因第一类Type undefined is not assignable to type ...。这个问题打开strictNullChecks后尤其常见。JS时代你习惯性地认为一个不存在的对象属性是undefined然后直接传给类型严格的参数。TS会当场拦住。解决方式是用可选链或显式判空const user getUser(); // 报错user 可能是 undefined // const name user.name; // 正确 const name user?.name ?? unknown;第二类Argument of type string is not assignable to parameter of type number。这种错误大多发生在后端返回的数据没做类型收窄就直接传给函数。后端返回JSON时全字段都是string或number混在一起前端这里经常要再做一次解析。改进方法是给接口数据定义明确类型在赋值处使用类型守卫收窄interface ApiResponse { code: number; data: unknown; } function parseResponse(res: ApiResponse) { if (typeof res.data string) { console.log(res.data.length); } else if (typeof res.data object res.data ! null) { // 对象分支 } }第三类Cannot find module或路径别名报错。这类问题不是类型写错是配置问题通常出在tsconfig的paths和打包器配置不一致上。检查顺序是先看tsconfig中paths是否正确再看打包器配置Vite的resolve.alias、webpack的resolve.alias最后检查文件后缀.vue、.ts、.d.ts导入时是否需要扩展名。第四类泛型写错导致的一堆连环报错。新手最容易犯的错是把泛型当“any”用写const data: any ...然后吐槽TS没起到作用。其实泛型要表达的是“这里应该是个什么形状”先用interface定义形状再让泛型去约束。写起来会慢一点但维护时候的体验完全不一样。4.2 运行时错误编译过了照样崩的三种场景TS只做编译期检查运行时错误依然存在只不过换了种形式。最常见的三种第一种是“接口返回字段和类型定义对不上”比如后端改了字段名你前端类型定义还是旧版运行时拿不到数据界面白屏。解决办法是在API层做数据校验或者至少保证接口文档和类型定义手动同步。第二种是“强制类型转换带来的错误”比如后端返回{ status: 0 }你定义status: number编译期通过运行时比较status 0永远为false因为字符串和数字在严格比较下不相等。第三种是“TS类型没覆盖到的地方”比如JSON.parse回来的对象永远不能完全相信它的结构。我有个习惯可以把这种经验总结成自己的小规则凡是外部数据接口返回、localStorage读取、用户输入进入内部逻辑前必须做一个“信任边界”的判断——要么用类型守卫手动校验要么用专门的校验库。这个习惯在JS时代就该有转TS后只是多了一层保障不能完全依赖类型系统。4.3 与构建工具相关的运行时坑我在迁移Vue2项目时遇到过一个很典型的问题TS编译通过但浏览器控制台报Cannot read properties of undefined。定位后发现是组件里访问了this.$refs.xxx在Vue的mounted钩子里该ref还没渲染完成。这类问题跟类型无关但TS的严格检查会让你比平时更早注意到“某个属性可能不存在”从而推动你加上判空逻辑这其实是好事。另一个坑是vite或webpack对TS文件的编译顺序。如果项目中存在循环引用TS编译器可能先处理A文件而A引用了BB又反向引用A导致某个类在初始化时是undefined。这种问题在JS时代就存在但TS的模块系统会把它暴露得更明显。解决方法很简单重构循环依赖或者使用延迟加载在函数内部require或动态import。5. 迁移实操从老JS项目到TS项目的完整步骤记录下面是我实际做过的一次迁移流程记录项目是一个基于Vue2 ElementUI的运营后台约两万行代码包含二十多个页面组件和十几个工具函数文件。这里直接给出可复制的操作步骤和过程中的一些判断依据。5.1 阶段一搭建TS环境让项目先跑起来第一步安装依赖。我在项目里用的是Vite所以需要装typescript、vue-tsc用于扫描Vue单文件组件内的TS代码。如果是webpack项目要装ts-loader或babel-preset-typescript并把ts-loader的配置加进module.rules。npm install -D typescript vue-tsc npx tsc --init第二步修改配置文件。把上面3.1节的tsconfig.json内容复制过去include里的路径改成项目实际结构。特别注意allowJs: true。Vite项目还需要确认vite.config.ts的resolve.alias和tsconfig的paths指向一致。第三步把所有.js文件改成.ts。这一步会爆出大量类型错误不要慌也不要急着全部修完。先把明确的类型标注补上函数参数、返回值、接口暂时搞不定的用any占位。我给自己定的规则是一个文件如果错误超过20个先全部用any让它能编译通过记在todo里后面单独处理少于20个当场修完。这样做是因为光看错误列表容易产生挫败感而分而治之能保持节奏。5.2 阶段二先改工具函数和API层再改组件迁移顺序很重要。我建议先改没有UI依赖的纯逻辑文件比如utils/format.ts、api/request.ts、store/modules/user.ts。这些文件内部逻辑独立、依赖少、容易出效果而且它们是整个项目的基础层基础层类型好了上层组件的类型自然就清晰了。以API层为例原来JS写的是export function fetchUserList(params) { return request.get(/api/user/list, { params }); }改为TS后// 先定义清晰的接口 export interface UserListParams { page: number; pageSize: number; keyword?: string; status?: active | disabled; } export interface UserItem { id: number; name: string; avatar: string; status: active | disabled; createdAt: string; } // 函数签名有完整的输入输出类型 export function fetchUserList( params: UserListParams ): PromiseApiResponseUserItem[] { return request.get(/api/user/list, { params }); }这样改完API层之后所有调用方在编辑器里就能直接看到返回数据的结构组件里的类型问题会大幅减少。这也是为什么先改底层的原因——上层引用底层时IDE能给出正确的类型推导改起来事半功倍。接着是组件层。Vue单文件组件SFC里要给props、emits、ref定义类型。Vue3的defineProps和defineEmits天然支持TS泛型script setup langts interface Props { user: UserItem; showDetail: boolean; } const props definePropsProps(); const emit defineEmits{ (e: update, id: number): void; (e: delete, id: number): void; }(); const visible ref(false); const currentUser refUserItem | null(null); /scriptVue2项目的话需要用Vue.extend或vue-class-component来标注类型技巧略有不同但“先定接口、再写组件”的大思路不变。5.3 阶段三清剿any定义统一的数据模型第一轮把项目跑通后我通常会集中一个周末做“清剿any”的工作。方法是全局搜索any逐个文件确认能不能用更精确的类型替代。很多any来自后端接口返回值这种情况建议在API层统一封装泛型。我常挂在嘴边的一句话是“项目里可以容忍少数any但不能容忍到处是any。”一个两个any像遗留在代码里的TODO注释数量多了就变成了“只管能用、根本不敢动”的代码。清剿any的过程也是对业务数据的重新梳理你会发现自己居然第一次搞清楚每个接口究竟返回了什么。5.4 阶段四开启checkJs把历史JS也纳入检查项目主体转完后我打开checkJs: true让编译器对剩余的历史JS文件做类型检查。注意这里不是让你立刻把所有JS文件改成TS而是先让编译器告诉你哪个JS文件存在问题。与此同时我还会给项目中少量仍然保留为.js的公共库文件加JSDoc注释用param标注参数类型。这算一个过渡手段最终目标仍然是把所有业务代码统一成.ts。// 一个典型的JSDoc标注 /** * 格式化时间戳为日期字符串 * param {number} timestamp - 毫秒时间戳 * param {string} pattern - 日期格式如 YYYY-MM-DD * returns {string} */ export function formatTime(timestamp, pattern YYYY-MM-DD) { // implementation }加了JSDoc之后编辑器对这些JS文件的提示也会变好。这一步的意义是让历史代码也受益于类型信息不需要一次性重构完但能享受一部分TS的开发体验。6. 从JS迁移到TS后我对类型编程的几条心得写到这里基本把JS转TS的技战术问题都讲完了。最后聊一些更“软”的东西它们来自我踩过的坑和后来的反思。6.1 TypeScript不是银弹它改变的是问题出现的位置转TS不是让bug消失而是把一部分隐性问题变成显性问题。以前用JS时一个字段拼写错误可能要等到页面白屏才被发现现在写代码时编辑器就帮你标出来了。但要注意如果你把类型检查当作“消灭报错”的手段很容易陷入“为了让编译器闭嘴而加as any”的陷阱。我的经验是看到类型报错先别急着修先想想“这个错误是否暴露了代码逻辑本身的问题”。比如“这个函数会返回undefined”可能意味着你并没有处理某个异常分支——先补逻辑再改类型顺序不要颠倒。6.2 渐进迁移最怕的不是类型难写而是配置不一致如果你问我迁移老项目最耗时的地方在哪我一定回答“配置文件对齐”。TS编译器、打包器、编辑器三方对路径别名的解析各有规则稍有不一致就是一堆“模块找不到”的报错。所以迁移前花半小时把tsconfig和Vite/webpack的配置对齐后面能省一整天。另外迁移项目最好同步更新团队里使用的ESLint配置配上typescript-eslint插件它能在写代码时给出更多规范建议尤其是对any的灰色地带。6.3 最后的实操建议如果你现在正准备把项目转到TS我建议你按下面的顺序做先挑一个非核心业务、依赖较少的模块做试点走完“配置变更→类型标注→编译通过→联调测试”的完整流程记录过程中所有卡点再推广到全项目。不要一开始就上全量迁移否则压力巨大且容易返工。试点模块选好迁移完成后把心得沉淀成团队wiki里面记录好常用的类型定义。这样后面的人看到的不只是代码还有一份活文档。最后再分享一个小技巧把tsconfig.json里的noUnusedLocals和noUnusedParameters打开。这两个选项会在编译阶段直接把“定义了但没用的变量”当错误报出来。对老项目来说这是清理多年技术债的绝佳机会——我迁完第一个模块后光靠这两个开关就删掉了三四十个历史遗留的无用参数。看着干净起来的代码那一瞬间你会觉得转TS这件事真的值。
返回列表