
前几年面试时,简历上没写 "熟悉 TypeScript" 都不好意思投前端岗。那时候圈里都在传:"不会 TS 的前端迟早被淘汰"。我抱着焦虑感连夜啃文档,从interface
学到泛型
,从类型断言
写到工具类型
,总算赶在风口上搭上了这班车。
可现在敲代码时,看着屏幕上密密麻麻的类型声明,我总会盯着光标发呆 ------ 这玩意儿怎么越用越累了?
那些被类型绑架的日常
上周改个用户列表页,后端说接口返回加了个lastLoginTime
字段。按以前用 JS 的习惯,直接user.lastLoginTime
就能渲染,顶多在控制台报个undefined
警告。
但现在得先找到User
接口定义,在里面加一行lastLoginTime?: string
;然后去改请求函数的返回类型,确保这个字段被正确包含;最后还要检查所有用到User
类型的组件,看看有没有地方因为新增可选字段导致类型报错。前后折腾半小时,实际渲染代码就加了一行。
后端同事路过时瞥了眼屏幕:"我就加个字段,你这改的比我还多?" 我只能苦笑 ------ 他永远不懂,为了让user.lastLoginTime
在编辑器里有正确的类型提示,我得付出多少额外工作。
更离谱的是上次处理表单组件。产品要做个动态表单,不同类型的字段有不同的校验规则。我光是定义类型就写了 200 多行:
ts
interface InputField {
type: 'input';
props: {
placeholder?: string;
maxLength?: number;
// ... 10多个属性
};
validator?: (value: string) => boolean;
}
interface SelectField {
type: 'select';
props: {
options: { label: string; value: string }[];
multiple?: boolean;
// ... 又是一堆属性
};
validator?: (value: string | string[]) => boolean;
}
// 还有DatePicker、Checkbox... 最后组合成联合类型
type FormField = InputField | SelectField | DateField | CheckboxField;
等类型终于定义完,发现业务逻辑代码才写了 150 行。那天加班到十点,回家路上满脑子都是类型"string"不能赋给类型"number | undefined"
的报错提示。
被神话的 "提前发现错误"
鼓吹 TS 的文章总说:"静态类型能在编译时发现错误,减少线上 bug"。这话没错,但现实往往是:我们花 80% 的精力解决的,都是 "类型错误" 而非 "业务错误"。
有次上线前,团队花了两天时间 "消灭红波浪线"。其中一半的错误是因为后端接口返回的status
字段,有时候是number
(1、2、3),有时候是string
("1"、"2")。我们没去深究接口为什么这么设计,而是在前端写了一堆:
ts
const status = res.status as unknown as number;
// 或者更离谱的
const status = Number(res.status) as 1 | 2 | 3;
最后线上还是出了 bug------ 不是因为类型不匹配,而是产品改了需求,某个状态的处理逻辑忘了同步更新。那些被我们精心维护的类型定义,在真正的业务漏洞面前毫无作用。
更讽刺的是,为了让 TS"满意",我们常常写出违背直觉的代码。比如处理空值时,明明可以if (data) { ... }
简单判断,却非要写成:
ts
if (data !== null && data !== undefined && Object.keys(data).length > 0) {
// TS终于不报错了
}
这种为了适配类型系统而写的冗余代码,反而降低了可读性,埋下了新的隐患。
前后端的 "类型鸿沟"
后端同事的口头禅是:"我接口文档都写了,你前端自己适配下就行"。他们永远理解不了,为什么一个字段从number
改成string
,前端要改半天。
有次和后端联调支付接口,对方突然说:" 用户 ID 我打算从id
改成userId
"。我当场眼前一黑 ------ 这个字段贯穿了从登录到支付的 12 个接口、7 个组件和 3 个工具函数。
那天下午我什么业务都没做,光是全局搜索id
,然后把所有相关的类型定义、接口声明、参数传递全部改了一遍。改到最后,后端又轻飘飘地说:"哦对了,还有个地方漏了,订单 ID 也得改"。
后来我们引入了 Swagger 自动生成 TS 类型,本以为能解放双手,结果生成的代码比我自己写的还离谱:
TS
export interface UserResponse {
code: number;
data: {
[key: string]: any;
id?: number;
name?: string;
// 一堆可选属性
};
message?: string | null | undefined;
}
这种看似严谨的类型定义,实际和any
没区别。最后还是得手动整理,等于做了两遍工作。
什么时候该松松绑?
前几天重构一个老项目,我试着用 JS 写了个模块。没有类型声明的束缚,思路反而顺畅了很多 ------ 不需要先构思完整的类型结构,想到哪写到哪,遇到问题直接在控制台调试,效率反而高了不少。
当然我不是说 TS 不好。在大型项目、团队协作时,它的类型约束确实能减少沟通成本。但我们是不是有点走火入魔了?
看到有人为了给setTimeout
加类型,写出这样的代码:
ts
const delay = <T>(ms: number, value: T): Promise<T> => {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms);
});
};
我突然意识到,我们可能把 TS 当成了银弹,却忘了它只是个工具。就像用扳手拧螺丝,合适的扭矩能拧紧零件,太用力反而会滑丝。
或许前端圈需要一点反思:当类型声明比业务逻辑还复杂时,当为了适配类型系统而扭曲代码时,当团队因为类型问题争论不休时 ------ 我们是不是该停下来,问问自己:我们写代码是为了实现功能,还是为了满足 TS 的校验规则?
毕竟,用户不会因为你代码里的类型定义完美无缺,就多付一分钱。