文章目录
SmartOA 前端是从 SmartAdmin V3 二次开发的,早期代码处于「能跑就行」的状态:157 个 vue-tsc 类型错误、大面积 any、API 返回类型全靠猜。2026 年 5 月底,我决定一次性把类型体系立起来——升级 TypeScript 5→6、vue-tsc 2→3,把 any 全部清零,让 vue-tsc --noEmit 从 157 个错误走到 0。这篇记录整个迁移的拆解方法和关键决策。
一、为什么必须做这次迁移
前端规模:254 个 Vue 文件 + 217 个 TS 文件,约 7.2 万行代码。在业务持续叠加的情况下,类型债只会越滚越大:
any泛滥 → 重构时编译器完全失去保护,改一个字段名要全局搜- API 返回类型未定义 → 前端拿到
res.data.xxx全靠运行时赌 - Vue 组件 props/emit 无类型 → 父子组件耦合靠文档和记忆
- 依赖版本旧(TS 5 / vue-tsc 2)→ 新语法特性(satisfies、const 泛型)用不了
💡 决策:未上线代码宁可一次性大规模迁移(长痛不如短痛),也不打补丁式修复。这次迁移是「先升级工具链,再修类型错误,最后清 any」三步走。
二、第一步:工具链升级 TS 5→6 + vue-tsc 2→3
先升级再修错,避免在旧编译器下重复劳动。升级内容:
// package.json 关键变更
"typescript": "^6.0", // 5 → 6
"vue-tsc": "^3.0", // 2 → 3
"@types/node": "^25.0", // 20 → 25
"node": ">=22" // 运行时要求同步提升
升级后立刻跑 vue-tsc --noEmit,错误数从 0(旧版根本没检查出来)直接飙升到 157——这正是想要的效果:新编译器把以前藏着的类型问题全部暴露了。
三、157 → 0 的拆解过程(checkpoint 记录)
没有一口气修完,而是按「错误聚类」分批推进,每批留下 checkpoint:
| 阶段 | 错误数 | 修复内容 |
|---|---|---|
| 起点 | 157 | 升级后全量暴露 |
| 第 1 批 | 157 → 62 | API 类型修复:PageResult 等通用返回结构 |
| 第 2 批 | 157 → 42 | login / center / help-doc / role-tree / role-menu 的 API 类型 |
| 第 3 批 | 157 → 39 | flow tasks VO casts + help-doc-form-drawer |
| 第 4 批 | 157 → 0 | 剩余 VO 转换与组件类型级联 |
关键技巧:先修公共类型(PageResult、分页参数),再修业务页面。公共类型一改,几十个页面的错误会同时消失,效率远高于逐个页面硬啃。
四、定义 VO 类型体系:不再靠猜
此前前端没有员工/角色/部门的类型定义,接口返回什么全靠 res.data 链式取值。这次补全了核心 VO:
// types/api/employee.ts — 与后端 EmployeeVO 一一对应
export interface EmployeeVO {
employeeId: number;
username: string;
nickname?: string;
departmentId?: number;
jobNumber?: string;
status: number; // 1 在职 / 0 离职
createTime?: string;
}
export interface RoleVO {
roleId: number;
roleName: string;
roleKey: string;
}
export interface DepartmentVO {
departmentId: number;
departmentName: string;
parentId?: number;
}
export interface PositionVO {
positionId: number;
positionName: string;
}
✅ 效果:VO 定义后,组件里的类型级联更新一次到位(commit: T-2/T-3),API 返回类型修正不再需要每个页面单独猜。
五、any 清零:三条硬规则
清 any 不是「把 any 改成 unknown 就完事」,而是建立规则:
- 禁用 any:unknown 必须收窄(类型守卫 / 断言 / 泛型),禁止
as any和as unknown as X绕过检查 - catch 语句:用
e as {message?: string}或e instanceof Error,不吞异常类型 - 根源修复:API 返回类型与后端 DTO 对齐(PageResult<T> 泛型),而不是在调用处打补丁
// ❌ 旧写法:any 随手用
const list = res.data as any;
// ✅ 新写法:泛型 + 类型守卫
const list = res.data as PageResult<EmployeeVO>;
if (!list || !Array.isArray(list.records)) throw new Error('数据格式异常');
六、顺带完成的周边清理
迁移过程中把同类问题一并处理:
lodash→lodash-es(ESM 按需引入,体积更小,P2-5)- Element Plus 改为按需引入(P2-6),配合 unplugin 自动导入
execCommand('copy')→ Clipboard API(已被废弃的旧 API)- 字典项类型
DictItem→DictDataItem命名对齐(T-6) - vue-shim.d.ts 补全全局属性类型声明
七、成果与数据
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| vue-tsc --noEmit | 157 errors | 0 errors |
| any 残留 | lib/plugins/directives/types 多处 | 0 处 |
| 构建耗时 | — | 3.12s |
| ESLint | 10 error + 9 warn | 0 问题 |
✅ 最终状态:vue-tsc 零错误、any 清零、ESLint 清零,类型体系成为后续所有前端改动的第一道防线。此后每次提交前
npx vue-tsc --noEmit + ESLint + Prettier 成为强制检查(六项检查中的前三项)。
八、踩坑清单(可直接复用)
- 先升级再修错:新编译器暴露的问题才是真问题,别在旧版本下浪费时间
- 先公共后业务:PageResult 这类公共类型一改,几十个页面错误同时消失
- checkpoint 留痕:每批修复记录错误数变化(157→62→42→39→0),便于回溯定位
- any 清零是规则不是动作:unknown + 类型守卫 + 泛型,禁止 as any 逃生
- VO 对齐后端:前端类型与后端 DTO 一一对应,字段名驼峰一致,减少转换层
评 论