TypeScript的类型系统里有47%的语法糖,是Java和C#程序员带着旧习惯来会直接撞墙的。Gabriel在最新技术博客里扔出一组数据:超过半数从强类型语言迁移过来的开发者,前三个月都在和「结构类型」较劲。
这不是能力问题,是肌肉记忆在作祟。
熟悉的陷阱:你以为是继承,其实是鸭子类型
Java程序员看到class User就手痒,TypeScript文档却建议你停手。Gabriel的原话很直接:「你会伸手去够类层级和抽象类。停。」
结构类型(structural typing)的核心规则是:只要形状对得上,名义上是不是一家人无所谓。
一个{ id: number, name: string }的对象,不需要显式实现任何接口,就能塞进要求该形状的函数参数里。这在Java里是天方夜谭——你必须先写implements User,再谈能不能传进去。
Gabriel把这种现象比作「租房市场的不成文规矩」。名义类型语言像严格的小区物业,只看房产证;结构类型像二房东,只要你能付押金、不闹事,谁管你户口本在哪。
这种差异导致一个经典翻车现场:重构时改了接口字段名,IDE没报错,运行时却炸了。因为TypeScript只检查结构,不追踪「这个对象当初是按哪个接口声明的」。
新工具箱:type和interface的暗战
TypeScript给了两把锤子:type别名和interface。新手教程通常说「差不多,随便用」,Gabriel的实践经验是——差很多。
先看表面相似性。定义对象形状,两者几乎逐字相同:
但type能表达联合类型(union types),interface不行。比如一个API响应可能是成功对象或错误对象,用type Response = Success | Error一行搞定,interface得绕远路。
更隐蔽的是声明合并(declaration merging)。interface Window可以写两遍,自动合并成一个定义。这在扩展第三方库的类型时有用,比如给浏览器全局对象打补丁。
Gabriel的使用策略很功利:「几乎全用type。只有需要声明合并,或者设计对外公开的API时,才换interface。」
他补充了一个数字:应用代码里需要声明合并的场景,占比不到5%。
行为变异:那些长得像、味道不同的语法
数组有两种写法:number[]和Array。功能完全一致,团队分歧却真实存在。Gabriel选前者,因为「短」。有些团队坚持后者,为了和其他泛型类型保持视觉统一。
他的建议是:「挑一个,继续写。」别在这个问题上开技术评审会。
更坑的是number类型。Java分int、float、double,TypeScript全塞进一个number。需要高精度时用bigint,但Gabriel说那覆盖了不到1%的场景。
函数重载是另一个认知陷阱。Java的重载是编译期多态,多个同名方法共存。TypeScript的overload signatures只是类型层面的花招,运行时只剩一个实现。Gabriel在前一篇文章里专门拆过这个:「那不是真正的重载。」
null和undefined的分裂同样折磨人。两个都表示「空」,但undefined是「声明了没赋值」,null是「故意设为空」。TypeScript的严格空检查(strictNullChecks)能把两者区分开,但旧代码迁移时,这开关一开就是几百个报错。
类型擦除:编译后的幽灵
所有类型注解在编译成JavaScript后都会消失。Gabriel把这比作「建筑图纸和实际房子的关系」——图纸指导施工,但住进去的人看不到承重墙的计算公式。
这意味着运行时类型检查必须自己动手,或者依赖zod这类库。TypeScript不会为你的API响应自动校验,它只保证「如果数据符合这个形状,后续代码不会踩类型雷」。
一个常见抱怨是:「我明明定义了接口,为什么传错数据还能跑?」答案就在类型擦除里。编译通过了,但数据来自运行时,TypeScript管不到那段。
Gabriel的收尾建议很产品经理思维:别在「type vs interface」上建宗派,把时间省下来写运行时校验。类型系统解决的是「假设数据干净时的逻辑正确性」,不是「数据真的干净」。这两件事的边界,是他见过最多团队踩坑的地方。
你在迁移TypeScript时,有没有遇到过「编译通过、运行翻车」的类型擦除陷阱?具体是哪个场景让你意识到「原来类型只活在编译期」?
热门跟贴