TypeScript仅在编译时检查代码自洽性,无法验证外部数据,运行时类型消失。开发者常因过度信任而跳过验证与测试,产生虚假安全感。真正的类型安全需在系统边界处严格校验,分离关注点,理解工具局限。建议借鉴艾尔姆语言的无逃生舱门设计,建立纪律性架构,通过Zod等库确保数据有效,而非盲目依赖类型注解。
这篇来自 Christian Ekrem 大神的原创作品,标题就叫《TypeScript为何救不了你》,直接把咱们前端圈子里最大的那个幻觉给撕得粉碎!
埃克雷姆这位老哥可不是什么无名小辈,他是真的在硅谷顶尖科技公司摸爬滚打多年的资深架构师,对类型系统、函数式编程、还有前后端架构有着极其深刻的理解和实战经验,他写过无数让咱们醍醐灌顶的技术文章,今天这篇更是把TypeScript这个被吹上天的工具直接放在了手术台上,一刀一刀解剖给大家看,让你们看看咱们每天赖以生存的类型检查,到底有多么的脆弱和不可靠!这篇文章首发在他个人的技术博客cekrem.github.io上面,发布时间是2025年11月6号!
我知道,看到这标题,很多兄弟已经开始生气了,心里肯定在想:我花了整整三年时间啊,把泛型、条件类型、映射类型这些高大上的玩意儿啃得滚瓜烂熟,你现在告诉我TypeScript没用?这不是要我命吗?
别急别急,埃克雷姆大神不是来攻击TypeScript这个工具本身的,他攻击的是咱们这些开发者脑子里那个根深蒂固的致命误解——以为有了类型就等于有了类型安全!以为编译器那个绿油油的小勾勾就是上帝在给你点头呢!错!大错特错!那个绿勾仅仅意味着你的代码自己跟自己没矛盾而已,根本不代表你的代码是正确的!更不代表它能正确处理用户输入、服务器返回、网络异常这些真实世界的脏数据!
(如果用过编译语言的人应该明白这个道理,语言自身自洽的,这是逻辑一致性基础知识)
我跟你们说,问题压根儿不在工具本身,工具是无辜的,真正出问题的是咱们这帮使用工具的人的思维方式!
我天天在代码审查里看到这种场景:某个年轻小伙子,啪一下写了个函数,然后自信满满地说"没事,类型系统会帮我检查的",然后就把边界情况、异常处理、数据验证这些真正要命的东西全给省了!
你们想想看,是不是每天都在干这种蠢事?TypeScript给了你一个安全感,但这个安全感是一个彻头彻尾的幻觉,是一个糖衣炮弹!它让你感觉飘飘然,让你觉得编译过了就万事大吉,但那个感觉和现实之间、编译时和运行时之间、你的代码和真实世界之间,存在着一条巨大的鸿沟,而这条鸿沟里,就住着那些让你半夜三点被老板电话叫醒、让你年终奖泡汤、让你在公司群里被点名批评的生产环境Bug!这些Bug不会因为你有类型注解就放过你,它们会在用户最多的那个周五晚上六点准时爆炸!
咱们先说说这个"逃生舱门"的问题,这可是TypeScript最阴险的地方!埃克雷姆大神给咱们看了一个超级常见的代码片段,我保证你们每个人都写过类似的玩意儿!他说,你看这个接口定义,多标准啊:
interface User { |
兄弟们,你们想想看,fetch那个接口返回的东西,它是个啥?它就是个any!它可以是null,可以是undefined,可以是一个错误对象,可以是服务器返回的完全不相干的数据结构,甚至可以是空字符串!但是TypeScript呢?它傻乎乎地相信了你在函数签名里写的那个Promise
埃克雷姆大神指出,这种通过返回类型进行的隐式类型转换,只是TypeScript无数逃生舱门中的一个!它还给你提供了any这个核弹级选项,给你@ts-ignore这种把错误扫到地毯下的垃圾铲,给你as unknown as T这种双重撒谎的终极骗术,还有那些根本无法验证的类型断言!在一个大型代码库里面,几十号人一起干活,你怎么保证没人偷偷用了这些作弊手段?你根本保证不了!你的代码安全性,就取决于那个最弱的any!就像木桶理论一样,最短的那块板决定了你的水位,而TypeScript的世界里,最不负责任的那一行as any决定了整个项目的类型安全水平!你们想想,是不是每次遇到类型报错不耐烦的时候,就随手写个as any让错误消失?然后说"以后再来重构",然后这个"以后"就永远不存在了?
说到这,我整个人鸡皮疙瘩都起来了!你们想想艾尔姆(Elm)那个语言,在艾尔姆里作弊是字面意义上的不可能!没有逃生舱门!没有后门!编译器说安全,那就是真安全!但TypeScript呢?它给你自由,但自由是把双刃剑,它让你能写出漂亮的类型代码,也让你能轻而易举地射穿自己的脚!埃克雷姆大神特别强调,艾尔姆的这种设计不是限制,而是一种保护,它强迫你必须用正确的方式处理问题,而不是走捷径!TypeScript的逃生舱门设计哲学是"相信开发者",但现实是开发者不值得被无条件相信,我们都会偷懒、会犯错、会在周五下午五点为了早点下班而走捷径!
接下来咱们聊聊更深层次的"边界问题",这是埃克雷姆大神文章里最核心的洞察!他说,TypeScript只知道你的代码长什么样,它对外界一无所知!每次数据进入你的系统,无论从API来、从用户输入来、从localStorage来、从URL参数来,这些都是不受信任的、肮脏的、可能带着病毒的数据!TypeScript根本没法验证它们!你给它写个类型断言,比如as User,那只是你的一厢情愿,是你在心里默默祈祷"希望服务器给的是我想要的数据",但佛祖听不见你的祈祷,TypeScript也听不见!它只是个静态分析工具,不是个运行时守护神!
更可怕的是现代前端开发的一个普遍现象:我们把框架代码和基础设施强耦合在一起!埃克雷姆大神举了一个典型得不能再典型的React组件例子,你们看看自己是不是每天就这么写的:
function UserProfile({ userId }: { userId: string }) { |
家人们,你们看没看出来这个组件干了多少件事?它同时在做:管理React特定的状态和生命周期、从网络获取数据、用类型断言转换数据、渲染UI!这四件完全不同领域的事情,被塞在一个函数组件里,像一锅大杂烩!基础设施层(数据获取)和框架层(hooks、effects)和表现层(UI渲染)和领域逻辑层(业务规则)全都搅和在一起,分都分不开!这就像你家的厨房、厕所、卧室、客厅全都是一个房间,你刷牙的时候旁边有人在炒菜,你睡觉的时候有人在马桶上解决问题!这不是TypeScript的问题,这是架构问题,但TypeScript让这个问题变得更糟糕,因为它让你误以为那个data as User是安全的,让你产生一种"我做过类型处理了"的错觉!你甚至觉得这个组件很简洁,很符合React的哲学,但实际上你是在给自己埋雷!
在艾尔姆语言里,架构是被强制分离的!埃克雷姆大神展示了艾尔姆怎么处理API数据:
type alias User = |
兄弟们,你们看到这个decodeUser函数的返回类型了吗?Result Error User!这个Result类型是艾尔姆的神器,它强制你必须处理成功和失败两种情况!编译器不会让你遗漏任何一种可能!一旦你通过了decoder得到了一个User,那它就保证是有效的!你的核心领域层只和这种安全数据打交道,类型系统会物理上阻止无效数据进入你的业务逻辑!你根本没办法在艾尔姆里写出一个接收User但不去处理解码错误的函数,编译器会直接拒绝对你的代码说"不"!
但是在TypeScript里,没有这种强制机制!你可以把未验证的数据传到任何地方:
// Infrastructure layer - gets raw data |
TypeScript能告诉你fetchUser可能返回的不是真正的User吗?它不能!它能告诉你domain层可能在处理无效数据吗?它也不能!它就像一个瞎了眼的门卫,你说你是User你就是User,它不会检查你的身份证!这就是TypeScript最大的问题,它只检查静态类型,不检查数据有效性!
当然,埃克雷姆大神也给了咱们解决方案:你可以用Zod或者io-ts这样的库来在边界处做验证!他展示了如何用Zod定义UserSchema:
import { z } from "zod"; |
兄弟们,这才是正确的做法!UserSchema.parse(data)这行代码才是真正的验证!它会检查data是否有id、name、email字段,会检查id是不是数字,会检查email格式是否正确!如果不对,它会抛出一个详细的错误信息!但注意,重点来了:你必须得记住这么做!TypeScript不会提醒你!你忘了写parse,编译器不会失败,代码照样能跑,只是多了个定时炸弹!在一个几十人的大团队里,有人忘了是迟早的事!你也可以考虑用Effect这样的函数式解决方案,但那就是另一个话题了,今天咱们先不展开!
这就引出了埃克雷姆大神最本质的洞察:运行时 vs 编译时!这个区别是理解TypeScript局限性的金钥匙!TypeScript在运行时完全消失了,编译时对很多现实问题也一无所知!你的代码在生产环境跑起来的时候,所有那些花里胡哨的类型注解都没了,剩下的就是纯JavaScript,那个动态的、无类型的、看到undefined就笑眯眯地帮你往下执行的JavaScript!TypeScript是个编译时工具,它只能检查你的代码内部是否自洽,但它没法检查你的代码是否符合现实世界的真相!除非你明确告诉它,否则它不关心什么架构分层,不关心你的领域层和外部的危险世界有什么区别!
艾尔姆的类型系统则从头到尾都被严格执行!Decoder不只是注解,它真的在运行!Maybe类型不只是文档,它强迫你处理空值!这个差距就是天和地的差距!在艾尔姆里,类型信息在编译时和运行时都保持一致性,而TypeScript的类型系统就像一张过期的地图,看起来有条有理,但实际上你已经开进了沼泽地!埃克雷姆大神特别指出,很多TypeScript开发者根本不知道这个区别,他们以为类型注解就是类型安全,以为编译通过就是质量保证,这是最危险的误解!
但埃克雷姆大神说,最深层次的问题还不是技术,是心态!TypeScript创造了一种虚假的安全感!他列出了开发者的五种致命罪行:1. 跳过验证因为"有类型呢";2. 不测试边界因为"编译器检查过了";3. 信任as断言因为赶时间;4. 加any让错误消失;5. 相信编译过了就能跑!这才是真正的危险!不是TypeScript坏,而是咱们把它当成了它不是的东西!TypeScript是个超级强大的linter,它能抓住拼写错误、重构失误、API误用,但它不是安全保证,不能替代思考,更不是真正的类型安全!这种心态问题比技术问题更难解决,因为它植根于人性,植根于我们对工具的天真信任!
最后埃克雷姆大神话锋一转:既然TypeScript救不了你,什么能救你?答案是:理解边界!在任何系统里,无论TypeScript还是艾尔姆,你都必须知道哪里是脏数据进来,哪里变成了干净数据!你需要基础设施层做验证,需要领域层假设数据有效!在艾尔姆里,语言强制这个架构;在TypeScript里,你只能自己建立纪律:在边界处验证或解析、创建无法通过无效数据构造的安全类型、禁止逃生舱门、分离关注点、测试异常路径!
他还提到了"编程作为手艺"的理念!好工匠理解工具的优缺点,锤子适合钉钉子,你别用来拧螺丝!TypeScript适合抓内部错误、提升重构安全性、文档化意图、改善开发体验,但它不适合验证外部数据、防止运行时错误、保证类型安全、保护你不受脏数据侵害!最好的代码来自思考的开发者,不是来自框架炒作的追随者!埃克雷姆大神特别强调,前端工程现在太缺乏真正的工程化和架构设计,大家都在追逐新框架、新语法糖,却忘了最基础的边界划分和错误处理!
埃克雷姆大神的建议是:如果你想知道真正的类型安全是什么感觉,那种"编译过了基本就能跑"的感觉,去试试艾尔姆!不一定是为了生产使用(虽然他自己在用而且爱得不行),而是为了学习!一旦体验过真正的类型安全,你在任何语言里都会建立更好的边界意识!他推荐阅读他的另一篇文章《给React开发者的艾尔姆入门》,那篇文章详细讲解了艾尔姆的架构如何改变你对前端开发的认知!
最后的判决是:TypeScript救不了你,但理解它的局限性可以救你!用它,享受它,但别盲目信任它!在边界验证,测试异常路径,建立好的架构!绿勾勾只代表自洽,不代表正确!最好的代码来自工匠,不是炒作追随者!记住埃克雷姆大神的这句话:你不是在写类型,你是在设计系统!类型只是工具,系统才是目的!