半年前,我决定在新项目里全面使用TypeScript 5.6。

那时候我对TypeScript的了解还停留在"给JavaScript加类型"的层面,觉得这有什么难的,不就是写个类型注解嘛。但真正用起来之后,我才发现自己太天真了。TypeScript的类型系统远比我想象的复杂,各种高级类型、泛型、条件类型、映射类型,学得我头都大了。

有好几次,我被类型错误折磨得想放弃,觉得还不如用JavaScript自在。但坚持下来之后,我慢慢找到了窍门,也体会到了类型安全的好处。现在,TypeScript已经成了我开发项目的首选,再也回不去纯JavaScript了。

这篇文章我想记录一下我学习和使用TypeScript 5.6的经历,从入门到想放弃,再到真正掌握。分享一些学习经验和避坑指南,给正在学习TypeScript或者被TypeScript折磨的朋友一些参考。

入门:觉得很简单

最开始学TypeScript的时候,我觉得很简单。

不就是给变量加个类型嘛,比如let name: string = '张三',函数参数和返回值加个类型,比如function add(a: number, b: number): number。这些基础的类型注解,看一遍就会了,觉得TypeScript也不过如此。

我还用了interface和type来定义对象的类型,比如:

interface User {
  id: number
  name: string
  age: number
}

定义好类型之后,编辑器会有代码提示,写错了会有红色波浪线提示,确实比纯JavaScript舒服很多。那时候我觉得,TypeScript就是这么简单,给所有东西加上类型就完事了。

我甚至觉得,那些说TypeScript难的人,是不是太笨了,这么简单的东西都学不会。现在想想,那时候的我真是太年轻了,只看到了TypeScript的冰山一角。

第一次碰壁:泛型

真正让我意识到TypeScript不简单的,是泛型。

最开始看到泛型的时候,我觉得这东西有什么用。比如function identity<T>(arg: T): T { return arg },这不是多此一举吗,直接用any不就行了。

后来做项目的时候,遇到了一个需求:写一个通用的API请求函数,传入不同的接口地址,返回对应的数据类型。如果用any,就失去了类型提示;如果为每个接口写一个函数,又太重复了。这时候我才想到泛型,原来泛型就是用来解决这种问题的。

async function request<T>(url: string): Promise<T> {
  const res = await fetch(url)
  return res.json()
}

interface User {
  id: number
  name: string
}

const user = await request<User>('/api/user/1')

用了泛型之后,user就有了正确的类型提示,编辑器能自动补全user的属性,写错了还会报错。这时候我才体会到泛型的强大。

但泛型也让我开始头疼了。泛型约束、泛型默认值、条件类型里的泛型推断,这些概念一个比一个复杂。特别是看到一些库里的复杂泛型,比如React的类型定义,看得我一脸懵,完全看不懂。

那时候我开始觉得,TypeScript好像没那么简单。

第二次碰壁:高级类型

如果说泛型只是让我觉得有点难,那高级类型就是让我想放弃了。

TypeScript的高级类型真的太多了,联合类型、交叉类型、条件类型、映射类型、模板字面量类型、infer推断,每一个都不简单,组合起来更是让人头大。

比如条件类型:

type IsString<T> = T extends string ? true : false

这个看起来还能理解,就是判断T是不是string类型。但如果嵌套起来,再加上infer,就完全看不懂了:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any

这个类型是用来获取函数返回值类型的,我看了好久才明白。infer关键字就像一个变量,在条件类型中声明一个类型变量,然后在true分支里使用。这个概念真的很绕,我反复看了很多遍才理解。

还有映射类型,比如Partial、Required、Readonly、Pick、Omit这些内置工具类型,看起来很方便,但自己写一个映射类型就很难了:

type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

这个是Partial的实现,看起来简单,但如果要写一个更复杂的映射类型,比如把所有属性变成可选的同时把类型变成string,就不知道怎么写了。

还有模板字面量类型,这个是TypeScript 4.1引入的,真的很强大,但也很复杂:

type EventName<T extends string> = `on${Capitalize<T>}`
type ClickEvent = EventName<'click'> // 'onClick'

用模板字面量类型可以做很多神奇的事情,比如根据字符串生成类型,但写起来也很烧脑。

那段时间,我每天都在和这些高级类型作斗争。看到类型报错就头疼,特别是那些长达几行的类型错误信息,看都看不懂。有好几次,我都想把类型改成any,或者干脆不用TypeScript了。

第三次碰壁:第三方库的类型问题

除了语言本身的复杂,第三方库的类型问题也让我很头疼。

有的第三方库没有自带类型定义,需要安装@types/xxx。但有时候@types的版本和库的版本对不上,类型定义有问题,用起来各种报错。

有的库虽然自带类型,但类型定义写得很糟糕,或者很复杂,用起来很不方便。特别是一些历史悠久的库,类型定义是后来加上去的,和实际的API对不上,经常需要自己写类型断言。

还有的库用了很多高级类型,类型定义看起来像天书。比如React的类型定义,一个简单的FC类型,展开之后有几十行,各种泛型和条件类型,看得人头晕眼花。

最让我崩溃的是,有时候类型错误出现在第三方库的代码里,不是我自己写的代码,但编译器就是报错。这种问题最难解决,因为你不能改第三方库的代码,只能想办法绕过,或者写类型声明文件扩展。

那段时间,我经常在项目里写很多// @ts-ignoreas any,来绕过类型错误。虽然这样能让编译通过,但也失去了TypeScript的意义。我开始怀疑,用TypeScript到底是为了什么,是为了写代码还是为了和类型作斗争。

转折点:慢慢找到窍门

就在我快要放弃的时候,事情出现了转机。

我开始静下心来,系统地学习TypeScript的类型系统。我找了一些好的教程和文章,从基础到高级,一个概念一个概念地啃。我还做了很多类型体操的练习题,比如type-challenges,虽然很烧脑,但做多了之后,对类型系统的理解越来越深了。

慢慢地,我发现那些以前看不懂的高级类型,现在能看懂了。那些以前写不出来的类型,现在能写出来了。类型报错也不再让我头疼了,我能看懂错误信息,知道问题出在哪里,怎么解决。

我还总结了一些学习TypeScript的窍门。

第一个窍门是,不要一开始就追求写复杂的类型。先用基础类型,把项目跑起来,遇到问题再慢慢优化。很多时候,简单的类型就够用了,不需要用太复杂的高级类型。

第二个窍门是,善用内置工具类型。Partial、Required、Pick、Omit、ReturnType、Parameters这些内置工具类型,能解决大部分常见的类型问题。先把这些工具类型用熟,再考虑自己写复杂类型。

第三个窍门是,多写多练。TypeScript的类型系统是需要练习的,光看教程是学不会的。多写代码,多遇到类型错误,多解决问题,慢慢就熟练了。type-challenges是一个很好的练习网站,从简单到困难,一步步提升。

第四个窍门是,学会看类型报错。TypeScript的报错信息虽然长,但其实很详细,会告诉你哪里错了,期望的类型是什么,实际的类型是什么。学会读报错信息,就能快速定位问题。

第五个窍门是,不要滥用any。any是逃生通道,偶尔用一下可以,但不要到处用。用了any就失去了类型保护,和用JavaScript没区别了。遇到类型问题,先想办法用正确的类型解决,实在解决不了再用any。

现在的我:享受类型安全

现在,我已经能用TypeScript 5.6熟练地开发项目了。

我会用泛型写通用的工具函数,会用条件类型和映射类型写复杂的类型推导,会写类型声明文件扩展第三方库的类型。类型报错已经很少能难住我了,大部分问题看一眼就知道怎么解决。

更重要的是,我体会到了TypeScript带来的好处。

第一个好处是代码更可靠了。有了类型系统,很多低级错误在编译阶段就能发现,比如传错参数、访问不存在的属性、拼写错误等。这些错误如果到了运行时才发现,排查起来很麻烦,而且可能已经造成了线上问题。

第二个好处是开发体验更好了。有了类型提示,编辑器的自动补全非常好用,写代码的时候不用记API,编辑器会提示你有哪些属性和方法。重构的时候也很放心,改了一个类型,所有用到的地方都会报错,不会漏掉。

第三个好处是代码更易维护了。类型就是最好的文档,看类型定义就能知道一个函数接收什么参数,返回什么值,一个对象有哪些属性。新人接手项目的时候,看类型就能快速了解代码结构,不用到处翻文档。

第四个好处是团队协作更顺畅了。有了类型约束,团队成员写代码的时候会更规范,不会随便传参、随便改结构。代码评审的时候,类型也能帮你发现很多问题,减少低级错误。

现在,我已经离不开TypeScript了。新开项目,第一反应就是用TypeScript。如果让我回去写纯JavaScript,我会觉得很不适应,没有类型提示,没有编译检查,总觉得心里不踏实。

TypeScript 5.6的新特性

说到TypeScript 5.6,这一版本也有一些不错的新特性。

第一个新特性是更严格的类型检查。TypeScript 5.6加强了对一些边界情况的类型检查,比如数组的at方法、字符串的at方法等,类型推导更准确了。虽然更严格的检查可能会让一些旧代码报错,但长期来看是好事,能让代码更可靠。

第二个新特性是性能优化。TypeScript 5.6的编译速度比上一代更快了,特别是大项目,编译时间明显缩短。类型检查的速度也提升了,编辑器的响应更快了。

第三个新特性是更好的JSX支持。对于React开发者来说,TypeScript 5.6对JSX的类型支持更好了,特别是对新的React 19的特性支持更完善。

第四个新特性是模板字面量类型的优化。TypeScript 5.6对模板字面量类型的推导做了优化,一些以前推导不出来的类型现在能推导出来了,性能也更好了。

总的来说,TypeScript 5.6是一个稳步提升的版本,没有颠覆性的变化,但在类型检查、性能、JSX支持等方面都有进步。如果你已经在用TypeScript,升级到5.6是很自然的选择。

给初学者的建议

最后,给正在学习TypeScript或者准备学TypeScript的朋友几点建议。

第一,不要被复杂的类型吓到。TypeScript的类型系统确实很复杂,但你不需要一开始就掌握所有东西。先学基础类型,能用起来就行,高级类型慢慢学。很多人就是一开始就去看那些复杂的类型体操,被吓到了,然后就放弃了。

第二,在实际项目中学习。不要只看教程不动手,找一个实际的项目,用TypeScript写,遇到问题再查资料。在实际项目中遇到的问题,比教程里的例子更能让你理解。

第三,从JavaScript项目渐进式迁移。如果你有一个JavaScript项目,不要想着一下子全改成TypeScript。可以先把文件后缀改成.ts,用最宽松的配置,慢慢加类型,一个文件一个文件地迁移。渐进式迁移的压力小很多,也更容易坚持。

第四,配置要循序渐进。最开始用TypeScript的时候,可以把strict关掉,用宽松的配置,先跑起来。等熟悉了之后,再慢慢打开strict、noImplicitAny等严格选项,一步步提升代码的类型质量。

第五,善用工具。VS Code对TypeScript的支持非常好,一定要用VS Code开发。还有eslint的typescript插件,能帮你发现更多问题。type-challenges网站可以用来练习类型体操,提升对类型系统的理解。

第六,不要追求完美的类型。不是所有的代码都需要完美的类型,有时候为了一个边缘情况写很复杂的类型,得不偿失。在类型安全和开发效率之间找到平衡,适合自己项目的就是最好的。

写在最后

从入门到想放弃,再到真正掌握,我用了大概半年的时间。

这半年里,我有过被类型错误折磨到崩溃的时刻,也有过写出一个复杂类型之后的成就感。TypeScript确实有学习曲线,特别是它的类型系统,比很多静态类型语言都要复杂。

但只要你坚持下来,跨过那个门槛,就会发现TypeScript的世界很广阔,类型安全带来的好处是实实在在的。现在的我,已经离不开TypeScript了,它已经成了我开发项目的标配。

当然,TypeScript也不是银弹,不是所有项目都适合用TypeScript。如果是一个很小的脚本项目,或者快速原型,用JavaScript可能更高效。但对于中大型项目,特别是团队协作的项目,TypeScript能带来的价值是巨大的。

如果你正在学习TypeScript,或者被TypeScript折磨得想放弃,我想告诉你:再坚持一下,跨过那个门槛,你会看到不一样的风景。

最后用一句话来结束这篇文章:"TypeScript不是为了让你写更多代码,而是为了让你写更少的bug。"

愿每一个前端开发者,都能在TypeScript的世界里找到类型安全的快乐。