TypeScript 5.8发布之后,我们团队把几个项目都升级了。作为一个用了好几年TypeScript的老前端,我本以为升级就是改改版本号的事情,没想到经历了一场从入门到放弃的心路历程。

这篇文章我想记录一下这段经历,从最开始学习TypeScript,到深入使用,再到升级5.8之后遇到的各种问题,以及最终的选择和思考。不吹不黑,客观地聊聊TypeScript的优点和痛点。

如果你正在用TypeScript,或者在考虑要不要用,希望这篇文章能给你一些参考。

初识TypeScript

先说说我是怎么开始用TypeScript的。

大概是四五年前,那时候TypeScript还不像现在这么普及,很多项目还是用JavaScript。我当时在的团队要做一个比较大的前端项目,代码量预计会很大,参与的人也多。团队讨论之后,决定用TypeScript,主要是看中了它的类型系统,能在编译阶段发现很多错误,适合大型项目的协作。

刚开始用的时候,说实话是有点抵触的。写惯了JavaScript的灵活,突然要写类型声明,觉得很麻烦。一个简单的函数,JavaScript里几行就写完了,TypeScript里要写一堆类型注解,代码量多了不少。

而且那时候TypeScript的类型推导还不够智能,很多地方都要手动写类型,写起来很啰嗦。遇到复杂的类型,比如泛型、条件类型、映射类型,更是一头雾水,经常要查半天文档。

但用了一段时间之后,我慢慢感受到了TypeScript的好处。最明显的是,重构的时候放心多了。以前用JavaScript,改一个函数的参数,要全局搜哪里调用了,生怕漏了什么地方。用了TypeScript之后,改完直接编译,哪里错了编译器会告诉你,不用自己一个个找。

还有就是代码提示更准确了。IDE能根据类型信息给出准确的代码提示,不用再去翻文档或者猜这个对象有什么属性。写代码的速度反而提升了。

就这样,我从抵触慢慢变成了接受,再到后来的喜欢。TypeScript成了我做前端项目的首选。

深入使用的收获

用了几年TypeScript之后,我越来越觉得它的类型系统很强大。

第一个收获是类型安全。TypeScript能在编译阶段发现很多低级错误,比如拼写错误、类型不匹配、传错参数等。这些错误如果留到运行时,排查起来很麻烦。有了TypeScript,很多错误在写代码的时候就被发现了,bug率明显下降。

第二个收获是代码可读性。有了类型注解,代码的自文档性大大提升。看一个函数的类型签名,就知道它接收什么参数、返回什么值,不用去看函数的具体实现。对于团队协作来说,这一点非常重要,新人看代码的成本降低了很多。

第三个收获是重构能力。大型项目的重构是家常便饭,有了TypeScript的类型检查,重构的风险大大降低。改完代码跑一遍类型检查,大部分问题都能发现。这让我们敢于做大规模的重构,而不是因为怕出问题就不敢动。

第四个收获是生态完善。现在TypeScript的生态已经非常完善了,主流的框架和库都有类型定义。npm上的大部分包都自带类型,或者有@types的类型包。第三方库的类型支持比以前好了太多,不用自己写类型声明了。

那几年,我几乎成了TypeScript的忠实拥护者,新项目一律用TypeScript,还经常劝身边的朋友用。

升级5.8的契机

TypeScript 5.8发布的时候,我们团队正在做一个新项目的技术选型。看到5.8有不少新特性,比如更智能的类型推导、更好的性能、新的类型操作符等,我们决定直接用5.8。

另外几个老项目也计划升级到5.8,主要是想用上新特性,同时统一团队的TypeScript版本。

本来以为升级是一件很简单的事情,改一下package.json里的版本号,跑一遍npm install就行了。没想到,升级的过程中遇到了各种问题,让我一度想放弃TypeScript。

遇到的第一个坑:类型推导变了

升级之后遇到的第一个问题是,TypeScript 5.8的类型推导策略变了,导致一些之前能通过的代码现在报错了。

比如有一个函数,我们之前是这么写的:

function getData(id: string) { const data = cache.get(id); if (data) return data; const newData = fetchFromServer(id); cache.set(id, newData); return newData; }

在旧版本里,TypeScript能正确推导出返回值的类型。但在5.8里,因为类型推导策略的调整,返回值的类型变成了联合类型,而且有一个分支是undefined,导致调用的地方报错。

我们花了不少时间去排查这些类型错误,有些是因为5.8的推导更严格了,有些是因为我们之前的代码本身就有问题,只是旧版本没有检查出来。

虽然这些问题最终都解决了,但花的时间比预期多很多。而且有些地方为了通过类型检查,不得不加了一些类型断言,代码反而变得更啰嗦了。

遇到的第二个坑:第三方库类型不兼容

第二个坑是第三方库的类型不兼容。

我们项目依赖了不少第三方库,升级到5.8之后,有几个库的类型定义和5.8不兼容,编译的时候报错。

有的库是因为用了5.8里废弃的类型语法,有的是因为类型定义本身有问题,在5.8更严格的检查下暴露出来了。

我们的解决办法是,能升级库的就升级,不能升级的就自己写类型覆盖,或者用@ts-ignore先忽略。但@ts-ignore用多了,类型检查就形同虚设了,这让我很纠结。

最麻烦的是一个比较冷门的库,已经很久没更新了,类型定义还是好几年前的。我们不得不自己fork了一份,修改了类型定义才能用。这花了我们不少时间,而且以后维护起来也很麻烦。

遇到的第三个坑:复杂类型的可读性

第三个坑是复杂类型的可读性问题。

为了充分利用TypeScript的类型系统,我们在项目里写了不少复杂的类型,比如条件类型、映射类型、模板字面量类型等。这些类型在功能上很强大,能实现很灵活的类型检查。

但问题是,这些复杂类型的可读性很差。过了几个月再回头看自己写的类型,经常看不懂是什么意思。团队里的新人更是一头雾水,要花很长时间才能理解。

有一次,一个同事写了一个非常复杂的类型,用了多层条件类型嵌套,功能确实很强大,但除了他自己,没人能看懂。后来他离职了,这个类型就成了一个黑盒,没人敢改,出了问题只能重新写。

我开始反思,类型系统的目的是什么?是为了提升代码的可维护性,还是为了炫技?如果一个类型复杂到没人能看懂,那它反而降低了可维护性,违背了用TypeScript的初衷。

遇到的第四个坑:编译速度

第四个坑是编译速度。

TypeScript的编译速度一直是被吐槽的点,项目越大,编译越慢。我们的项目有几十万行代码,全量编译一次要好几分钟。虽然有增量编译,但有时候改了一个类型定义,会导致大量文件重新编译,等很久才能看到结果。

5.8虽然在性能上有优化,但对于大型项目来说,编译速度还是不够快。特别是在CI/CD流程里,类型检查这一步经常是瓶颈,拖慢了整个构建流程。

我们试过用一些加速方案,比如用esbuild做转译、用tsc做类型检查分开进行,或者用一些第三方的类型检查工具。但这些方案都有各自的问题,不是完美的解决方案。

有时候我会想,为了类型安全,付出这么高的编译成本,到底值不值?

遇到的第五个坑:any和类型断言的泛滥

第五个坑是any和类型断言的泛滥。

理想中的TypeScript项目,应该是全程类型安全,很少用any。但实际项目中,为了赶进度,或者为了快速解决类型错误,很多人会随手写一个any,或者加一个类型断言。

我们项目里就有不少这样的代码。刚开始只是一两个地方用any,后来越来越多,最后any到处都是。类型检查的效果大打折扣,很多地方和写JavaScript没什么区别。

我也反思过这个问题。为什么会这样?一方面是因为有些第三方库的类型确实不好用,不用any就搞不定;另一方面是因为团队的类型意识不够,遇到类型错误第一反应是用any绕过,而不是去解决根本问题。

但不管原因是什么,any泛滥的结果就是,TypeScript的优势大打折扣。花了那么多功夫搭的类型系统,最后形同虚设,这让人很有挫败感。

想放弃的瞬间

在升级5.8的过程中,有好几个瞬间我真的想放弃TypeScript。

有一次,为了一个复杂的类型错误,我查了整整一天的文档,试了各种方法,最后发现是TypeScript本身的一个bug。那一刻我真的很崩溃,觉得自己在和工具较劲,而不是在写业务代码。

还有一次,项目上线前发现一个类型错误,排查之后发现是因为类型推导太复杂导致编译器卡住了。为了让编译通过,我们不得不简化类型,牺牲了一些类型安全。那时候我在想,用TypeScript到底是在提升效率还是在降低效率?

还有一次,一个新人入职,看了我们项目里的复杂类型之后说,这比业务逻辑还难理解。那一刻我意识到,我们可能把TypeScript用偏了,为了类型而类型,反而增加了代码的复杂度。

这些瞬间,让我对TypeScript的信念产生了动摇。

重新思考TypeScript的定位

冷静下来之后,我开始重新思考TypeScript的定位。

我意识到,TypeScript是一个工具,工具的目的是提升效率和质量。如果一个工具让你花了大量时间在和工具本身较劲,而不是在解决业务问题,那这个工具的使用方式可能出了问题。

TypeScript的类型系统很强大,但不是所有地方都需要用最复杂的类型。简单的场景用简单的类型,复杂的场景才用复杂的类型。不要为了炫技而写复杂类型,也不要追求100%的类型覆盖率。

对于一些确实很难做类型检查的地方,比如和第三方库交互、动态生成的对象等,用any也不是什么大不了的事情。关键是要控制any的范围,不要让它扩散到整个项目。

还有就是,TypeScript的类型检查是辅助手段,不是银弹。不要以为有了类型检查就不会有bug了,单元测试、集成测试、代码审查这些质量保障手段依然不能少。

想清楚这些之后,我对TypeScript的态度变得更理性了。不再盲目追求类型的完美,而是在类型安全和开发效率之间找平衡。

最终的选择

最终,我们没有放弃TypeScript,但调整了使用方式。

第一,简化了很多复杂类型。把那些没人能看懂的复杂类型,改成了更简单、更易读的写法。虽然类型覆盖率稍微降了一点,但代码的可维护性提升了很多。

第二,制定了any使用规范。明确了哪些场景可以用any,哪些场景不能用,并且在代码审查中检查any的使用。这样既保留了灵活性,又防止了any的泛滥。

第三,拆分了项目。把一个大项目拆成了几个小项目,每个项目的代码量小了,编译速度快了很多,类型检查也更快了。

第四,升级了工具链。用了一些加速TypeScript编译的工具,比如增量编译、并行构建等,编译速度有了明显提升。

第五,加强了团队培训。定期组织TypeScript的分享和培训,提升团队的类型意识,让大家知道怎么写好类型,而不是遇到问题就用any。

调整之后,TypeScript用起来舒服多了。类型错误少了,编译速度快了,代码也更易读了。虽然还有一些小问题,但整体体验好了很多。

TypeScript的优点

说了这么多问题,还是要客观地说说TypeScript的优点。

第一,类型安全。这是TypeScript最大的优势,能在编译阶段发现很多错误,降低线上bug率。

第二,代码可读性。类型注解让代码更自文档化,团队协作更顺畅。

第三,重构更安全。有类型检查做保障,大规模重构的风险大大降低。

第四,生态完善。现在TypeScript已经是前端的主流,各种框架和库都有很好的支持。

第五,职业发展。会TypeScript已经是前端工程师的基本要求了,掌握TypeScript对职业发展有帮助。

TypeScript的痛点

再总结一下痛点。

第一,学习曲线陡。TypeScript的类型系统很复杂,深入掌握需要花不少时间。

第二,编译速度慢。大型项目的编译速度是个问题,影响开发体验。

第三,第三方库类型问题。有些第三方库的类型定义质量不高,或者不兼容新版本。

第四,复杂类型可读性差。过度使用复杂类型会降低代码的可维护性。

第五,any泛滥。如果团队类型意识不够,很容易出现any泛滥,让类型检查形同虚设。

给后来者的建议

最后给正在用或者准备用TypeScript的朋友几个建议。

第一,不要追求完美。不要追求100%的类型覆盖率,也不要写太复杂的类型。在类型安全和开发效率之间找平衡。

第二,从简单开始。刚开始用的时候,先用基础类型,不要一上来就玩复杂的泛型和条件类型。熟练了之后再逐步深入。

第三,制定规范。团队要制定TypeScript的使用规范,包括any的使用、类型命名、复杂类型的使用等,并且在代码审查中执行。

第四,关注编译性能。项目大了之后要关注编译速度,及时做优化,比如拆分项目、用增量编译等。

第五,理性看待。TypeScript是工具,不是信仰。它能提升代码质量,但不是银弹。不要因为用了TypeScript就忽视了测试和代码审查。

写在最后

从入门到放弃,再到重新接受,这就是我和TypeScript的故事。

TypeScript是一个强大的工具,但也是一个需要理性使用的工具。用好了,它能大大提升代码质量和开发效率;用不好,它会成为你的负担,让你每天和类型错误较劲。

TypeScript 5.8带来了很多新特性,也带来了一些新的问题。但总体来说,它依然是目前前端开发最好的选择之一。关键是你怎么用它。

最后用一句话来结束这篇文章:"没有最好的技术,只有最合适的技术。用对了,就是好技术。"

愿每一个前端开发者,都能找到适合自己的技术栈,写出高质量的代码。