我用TypeScript已经三年了,从最开始的抗拒,到慢慢接受,再到现在的喜欢,经历了一个很长的过程。

最开始接触TypeScript的时候,我是抗拒的,觉得JavaScript用得好好的,为什么要加个类型,写起来麻烦,编译也麻烦,纯粹是多此一举。后来,项目越来越大,团队人越来越多,JavaScript的问题越来越明显,bug越来越多,维护越来越难,才开始认真用TypeScript。

这三年里,踩了不少坑,也走了不少弯路,从最开始的满屏any,到慢慢学会用类型,再到现在能写出比较优雅的类型,慢慢才明白一些道理。今天这篇文章,就来分享一下我用了三年TypeScript之后,才明白的一些道理,希望能给正在学TypeScript,或者用了一段时间但还没入门的朋友一些参考。

一、类型不是负担,而是工具

我最开始用TypeScript的时候,最大的误区,就是把类型当成了负担,觉得写类型是额外的工作,是在浪费时间,能不写就不写,能写any就写any。

那时候写代码,经常是这样的:

function getData(data: any) {
  return data.list.map((item: any) => item.name)
}

满屏的any,类型写了跟没写一样,TypeScript变成了AnyScript,完全没有发挥出类型的作用。

后来,项目出了几次bug,都是因为类型不对,比如接口返回的数据结构变了,前端没有及时更新,导致运行时报错;或者同事传参传错了,把字符串传成了数字,导致计算错误。这些bug,如果有严格的类型检查,在编译阶段就能发现,不会等到运行时才出问题。

从那以后,我才开始认真对待类型,慢慢明白,类型不是负担,而是工具,是帮助我们写出更健壮、更易维护代码的工具。

类型的好处,主要有这几个:

  1. 编译时检查错误:很多低级错误,比如拼写错误、传参错误、类型不匹配,在编译阶段就能发现,不会等到运行时才出问题,大大减少了bug。
  2. 更好的代码提示:有了类型,编辑器能给出更准确的代码提示,自动补全,跳转定义,查看类型,开发体验好很多,也能减少很多低级错误。
  3. 更好的可维护性:类型就是文档,看类型就知道函数的参数和返回值是什么,数据结构是什么样的,不需要去读代码,也不需要去问别人,维护起来方便很多。
  4. 更好的重构支持:有了类型,重构的时候,改了一个地方,编译器会告诉你所有受影响的地方,不会漏改,重构起来更安全,也更放心。

当你真正体会到这些好处的时候,就会觉得,写类型不是负担,而是在帮你节省时间,减少bug,提高效率。

二、不要滥用any,但也不要完全排斥any

很多人学TypeScript的时候,会走两个极端,一个是满屏any,完全不用类型;另一个是完全排斥any,觉得用any就是原罪,就是不专业。

我自己也走过这两个极端,最开始是满屏any,后来知道了any的坏处,就开始完全排斥any,觉得写any就是水平不行,想尽办法不用any,结果写出来的类型很复杂,很绕,维护起来更麻烦。

用了三年之后,我才明白,any不是洪水猛兽,该用的时候还是要用,关键是不要滥用。

什么时候该用any:

  1. 快速原型开发的时候:刚开始做原型,需求还不明确,数据结构还没定,这时候可以先用any,快速把功能做出来,等需求稳定了,再补类型。
  2. 第三方库没有类型定义的时候:有些老的第三方库,没有TypeScript的类型定义,这时候可以先用any,或者自己写简单的类型声明,不要为了类型而类型。
  3. 类型太复杂,得不偿失的时候:有些场景,类型非常复杂,要写很多类型体操才能实现,而且维护成本很高,这时候,用any可能是更务实的选择,不要为了类型而类型。
  4. 动态性很强的场景:有些场景,数据结构是动态的,不确定的,比如解析任意的JSON,这时候,用any或者unknown更合适,不要强行写类型。

什么时候不该用any:

  1. 核心业务逻辑:核心的业务逻辑,数据结构明确,一定要写类型,不要用any,这是类型检查的重点,能避免很多严重的bug。
  2. 公共API和组件:公共的函数、组件、库,对外的接口,一定要写类型,因为用的人多,类型能帮使用者正确使用,也能减少沟通成本。
  3. 数据结构明确的地方:只要数据结构是明确的,就应该写类型,不要偷懒用any,写类型花不了多少时间,但能避免很多问题。

所以,any不是不能用,而是不要滥用,该用的时候用,不该用的时候不用,根据实际情况,灵活选择,这才是成熟的开发者该有的态度。

三、泛型是TypeScript的灵魂,一定要学会

很多人用TypeScript,用了很久,还停留在基础类型的层面,不会用泛型,觉得泛型很难,很复杂,不敢用。

我自己也是,最开始用TypeScript的时候,泛型一直没搞懂,觉得很抽象,不知道什么时候该用,也不知道怎么用,写的类型都是固定的,很死板,复用性很差。

后来,项目里需要写一些通用的工具函数和组件,才不得不学泛型,学了之后,才发现泛型真的是TypeScript的灵魂,学会了泛型,TypeScript的水平会提升一个档次。

什么是泛型?

简单来说,泛型就是类型的参数,把类型当成一个参数,在使用的时候传入,这样,同一个函数或者组件,就能支持不同的类型,提高复用性。

举个简单的例子,一个identity函数,返回传入的参数:

// 不用泛型,只能支持number类型
function identity(arg: number): number {
  return arg
}

// 用泛型,支持任意类型
function identity<T>(arg: T): T {
  return arg
}

identity(1) // 返回number类型
identity('hello') // 返回string类型
identity({ a: 1 }) // 返回{a: number}类型

用了泛型之后,这个函数就能支持任意类型,而且返回值的类型和参数的类型是一致的,类型安全,复用性也高。

泛型的常见应用场景:

  1. 通用工具函数:比如防抖、节流、深拷贝、数组去重等工具函数,用泛型,就能支持任意类型的数据。
  2. 通用组件:比如通用的列表组件、表单组件、弹窗组件,用泛型,就能支持不同的数据类型。
  3. 通用的类型工具:比如Partial、Required、Pick、Omit等,都是用泛型实现的,能方便地操作类型。
  4. API请求封装:封装请求函数,用泛型指定返回值的类型,这样,调用的时候,就能知道返回值是什么类型,类型安全。

学会泛型之后,你会发现,很多以前觉得很复杂、很难复用的类型,都能很优雅地实现,代码的复用性和可维护性都会大大提升。

当然,泛型确实有一定的学习成本,尤其是高级的泛型用法,比如条件类型、映射类型、模板字面量类型等,确实比较复杂,但只要多练多用,慢慢就能掌握,不要因为难就放弃,泛型值得你花时间去学。

四、不要过度追求类型体操,实用最重要

学了泛型和高级类型之后,很多人会陷入另一个误区,就是过度追求类型体操,写很复杂、很绕的类型,觉得类型越复杂,水平越高,越专业。

我自己也有过这个阶段,学了条件类型、映射类型、infer推断之后,就开始写各种复杂的类型,觉得很有成就感,但是后来发现,很多类型写得太复杂了,过一段时间,自己都看不懂了,更别说别人了,维护起来很麻烦。

用了三年之后,我才明白,类型是为业务服务的,不是为了炫技,实用最重要,不要过度追求类型体操。

什么时候该用复杂类型:

  1. 公共的类型工具:如果是公共的、会被很多地方用到的类型工具,比如通用的Partial、Pick等,值得花时间写好,写复杂一点也没关系,因为用的人多,收益大。
  2. 类型安全很重要的场景:有些场景,类型安全很重要,比如核心业务逻辑、公共API,值得花时间写更精确的类型,避免bug。
  3. 能大大提高开发效率的场景:有些复杂类型,虽然写的时候花时间,但用起来能大大提高开发效率,减少重复代码,这样的复杂类型是值得的。

什么时候不该用复杂类型:

  1. 一次性的、局部的类型:只在一个地方用一次,而且逻辑不复杂,就不要写太复杂的类型,简单明了就好,维护成本低。
  2. 业务逻辑经常变的地方:业务逻辑经常变,类型也会经常变,写太复杂的类型,改起来很麻烦,得不偿失,不如简单一点。
  3. 团队成员水平参差不齐的时候:如果团队里有人对TypeScript不太熟,写太复杂的类型,别人看不懂,维护不了,反而会成为负担。

类型的目的,是帮助我们写出更健壮、更易维护的代码,不是为了炫技,也不是为了复杂而复杂。好的类型,应该是清晰、简洁、实用的,让人一看就懂,用起来方便,而不是让人看不懂,维护起来麻烦。

所以,不要过度追求类型体操,实用最重要,根据实际情况,选择合适的复杂度,这才是成熟的态度。

五、TypeScript不是万能的,它不能替代测试和代码质量

很多人用了TypeScript之后,会有一个误区,觉得有了类型检查,代码就不会有bug了,就不需要测试了,也不需要太关注代码质量了。

我自己也有过这个阶段,觉得TypeScript能检查出大部分错误,有了类型,代码就很安全了,测试也可以少写一点。但后来发现,TypeScript不是万能的,它只能检查类型相关的错误,很多其他的错误,它是检查不出来的。

TypeScript检查不出来的错误:

  1. 逻辑错误:代码的逻辑写错了,比如条件写反了,计算错了,只要类型是对的,TypeScript就不会报错,但运行的时候结果是错的。
  2. 运行时错误:比如接口返回的数据结构和预期不一样,null或者undefined,数组越界,这些运行时的错误,TypeScript是检查不出来的,除非你做了运行时的类型校验。
  3. 性能问题:代码写得效率低,有性能问题,TypeScript是检查不出来的,需要你自己去优化。
  4. 安全问题:比如XSS、SQL注入等安全问题,TypeScript也是检查不出来的,需要你自己去防范。

所以,TypeScript只是一个工具,能帮我们减少一部分错误,但不能替代测试,也不能替代代码质量。有了TypeScript,还是要写测试,还是要关注代码质量,还是要做代码审查,这些都不能少。

而且,TypeScript的类型检查,是基于你写的类型的,如果你写的类型不对,或者写了any,那类型检查就没有意义了。所以,TypeScript的效果,取决于你用得好不好,不是用了TypeScript,代码就一定好。

正确的态度是,把TypeScript当成提高代码质量的工具之一,配合测试、代码审查、规范等,一起提高代码质量,而不是依赖TypeScript,觉得有了它就万事大吉了。

六、渐进式采用,不要一步到位

很多团队在引入TypeScript的时候,想一步到位,把所有的JavaScript代码,都改成TypeScript,结果发现工作量太大,问题太多,推进不下去,最后不了了之。

我自己也经历过团队引入TypeScript的过程,最开始也是想一步到位,结果发现,老代码太多,类型不明确,改起来很麻烦,还容易引入新的bug,推进得很艰难。

后来,我们调整了策略,采用渐进式的方式,慢慢引入,才顺利地把项目迁移到了TypeScript。

渐进式采用TypeScript的方法:

  1. 新代码用TypeScript:新写的代码,新的模块,都用TypeScript,老代码暂时不动,这样,新代码有类型检查,老代码也不需要改,风险小,推进快。
  2. 老代码逐步迁移:在修改老代码的时候,顺便把它改成TypeScript,或者在有时间的时候,逐步把核心的、经常改的模块,迁移到TypeScript,不需要一次性全改。
  3. 允许JavaScript和TypeScript共存:TypeScript是支持JavaScript的,可以允许.js和.ts文件共存,慢慢迁移,不需要一步到位。
  4. 先宽松后严格:最开始,可以把tsconfig的配置调宽松一点,比如不开strict,允许隐式any,减少迁移的阻力,等大家都熟悉了,再慢慢把配置调严格,提高类型检查的强度。

渐进式采用的好处是,风险小,阻力小,能慢慢看到效果,团队也能慢慢接受,比一步到位容易推进得多。

而且,TypeScript本身就是渐进式的,它的设计就是支持渐进式采用的,不需要一步到位,根据团队的情况,慢慢推进就好。

七、写在最后

用了三年TypeScript,从最开始的抗拒,到现在的喜欢,经历了很多,也明白了很多道理。

TypeScript是一个很好的工具,它能帮我们写出更健壮、更易维护的代码,尤其是在大型项目和团队协作中,作用很明显。但它也不是银弹,不是用了就万事大吉,它有它的适用场景,也有它的局限性,需要我们正确地认识和使用。

最重要的是,不要把类型当成负担,也不要把类型当成炫技的工具,要把它当成帮助我们写出更好代码的工具,实用为主,灵活运用,这样,才能真正发挥TypeScript的价值。

希望这篇文章,能给正在学TypeScript,或者用了一段时间但还没入门的朋友一些参考,少走一些弯路,更快地掌握TypeScript,写出更好的代码。

也欢迎大家在评论区,分享自己使用TypeScript的经验和感悟,一起交流讨论。