TypeScript 渐进迁移指南
Nathaniel 原作,翻译转载自 New Frontend。
我在大概一年前写了一篇[如何把 Node.js 项目从 JavaScript 迁移到 TypeScript][node-ts] 的指南。指南的阅读量超过了七千,不过其实当时我对 JavaScript 和 TypeScript 的了解并不深入,把重心更多地放到特定工具上,而没怎么从全局着手。最大的问题是我没有提供迁移大型项目的解决方案。显然,大型项目不可能在短时间内重写一切。因此,我很想分享下我最近学到的迁移项目到 TypeScript 的主要经验。
迁移一个包含成千上百个文件的大型项目可能比你想象得要容易。整个过程主要分 3 步。
注意:本文假定你已经有一定的 TypeScript 基础,同时使用 Visual Studio Code,否则,一些地方可能不一定直接适用。
相关代码:https://github.com/llldar/migrate-to-typescript-the-advance-guide
开始引入类型
花了 10 个小时使用 console.log
排查问题后,你终于修复了 Cannot read property 'x' of undefined
问题,出现这个问题的原因是调用了可能为 undefined
的某个方法,给了你一个「惊喜」!你暗暗发誓,一定要把整个项目迁移到 TypeScript。但是看了看 lib
、util
、components
文件夹里上万个 JavaScript 文件,你对自己说:「等以后吧,等我有空的时候。」当然那一天永远也不会到来,因为总有各种酷炫的新特性等着加到应用,客户也不会因为项目是用 TypeScript 写的就出大价钱。
如果我告诉你,你可以增量迁移到 TypeScript 并立刻从中受益呢?
添加神奇的 `d.ts`
d.ts
是 TypeScript 的类型声明文件,其中声明了代码中用到的对象和函数的各种类型,不包含任何具体的实现。
假定你在写一个即时通讯应用,在 user.js
文件里有一个 user
变量和一些数组:
那么对应的 user.d.ts
会是:
然后 message.js
里定义了一个函数 sendMessage
:
那么 message.d.ts
中相应的类型会是:
不过,sendMessage
也许没那么简单,参数的类型可能更复杂,也可能是一个异步函数。
你可以使用 import
引入其他文件中定义的复杂类型,保持类型文件简单明了,避免重复。
注意:我这里同时使用了 type
和 interface
,这是为了展示如何使用它们。你在项目中应该主要使用其中一种。
连接类型
现在已经有类型了,如何搭配 js
文件使用呢?
大体上有两种方式:
Jsdoc typedef import
假设同一文件夹下有 user.d.ts
,可以在 user.js
文件中加入以下注释:
确保 d.ts
文件中有相应的 import
和 export
语句,这一方式才能正确工作。否则,最终会得到 any
类型,显然 any
类型不会是你想要的。
三斜杠指令
在无法使用 import
的场景下,三斜杠指令是导入类型的经典方式。
注意,你可能需要在 eslint 配置文件中加入以下内容以免 eslint 把三斜杠指令视为错误:
假设 message.js
和 message.d.ts
在同一文件夹下,可以在 message.js
文件中加入以下三斜杠指令:
然后给 sendMessage
函数加上以下注释:
接着你会发现 sendMessage
有了正确的类型,IDE 能自动补全 from
、to
、message
和函数的返回类型。
或者你也可以这么写:
这是 jsDoc
书写函数签名的风格,肯定没有上一种写法那么简短。
使用三斜杠指令时,应该在 d.ts
文件中移除 import
和 export
语句,否则无法工作。如果你需要从其他文件中引入类型,可以这么写:
这一差别背后的原因是 TypeScript 把不含 import
和 export
语句的 d.ts
文件视作环境(ambient)模块声明,包含 import
和 export
语句的则视为普通模块文件,而不是全局声明,所以无法用于三斜杠指令。
注意,在实际项目中,选择以上两种方式中的一种,不要混用。
自动生成 `d.ts`
如果项目的 JavaScript 代码中已经有大量 jsDoc
注释,那么你有福了,只需以下一行命令就能自动生成类型声明文件:
```shell-session
npx typescript src/*/.js --declaration --allowJs --emitDeclarationOnly --outDir types
否则 *.d.ts
文件会被编译为 *.d.js
文件,这毫无意义。
现在你应该就能享受到 TypeScript 的益处了(自动补全),无需额外配置 IDE,也不用修改 js 代码的逻辑。
类型检查
如果项目中 70% 以上的代码都经过以上步骤迁移后,你可以考虑开启类型检查,进一步帮助检测代码中的小错误和问题。别担心,你仍将继续使用 JavaScript,也就是说不用改动构建过程,也不用换库。
开启类型检查的主要步骤是在项目中加上 jsconfig.json
。例如:
关键在于 checkJs
需要为真,这就为所有项目开启了类型检查。
开启后可能会碰到一大堆报错,可以逐一修正。
渐进类型检查
// @ts-nocheck
如果你希望以后再修复一些文件的类型问题,可以在文件头部加上 // @ts-nocheck
,TypeScript 编译器会忽略这些文件。
// @ts-ignore
如果只想忽略某行而不是整个文件的话,可以使用 // @ts-ignore
。加上这个注释后,类型检查会忽略下一行。
使用这两个标记可以让你慢慢修正类型检查错误。
第三方库
维护良好的库
如果用的是流行的库,那 DefinitelyTyped
上多半已经有类型定义了,只需运行以下命令:
```shell-session
yarn add @types/yourlibname --dev
注意:如果库属于某组织,库名中包含 @
和 /
,那么在安装相应的类型定义文件时需要移除 @
和 /
,并在组织名后加上 __
,例如 @babel/core
改为 babel__core
。
纯 JS 库
如果用了一个作者 10 年前就已经停止更新的 js
库怎么办?大多数 npm 模块仍然使用 JavaScript,没有类型信息。添加 @ts-ignore
看起来不是一个好主意,因为你希望尽可能地确保类型安全。
那你就需要通过创建 d.ts
文件增补模块定义,建议创建一个 types
文件夹,加入自己的类型定义。然后就可以享受类型安全检查了。
完成这些步骤后,类型检查应该能很好地工作,可以避免代码出现很多小错误。
类型检查升级
修复 95% 以上类型检查错误并确保每个库都有相应的类型定义后,你可以进行最后一步:正式把整个项目的代码迁移到 TypeScript。
注意:我上一篇指南 中提到的一些细节这里就不讲了。
把所有文件改为 `.ts` 文件
现在是时候把 d.ts
文件和 js 文件合并了。由于几乎所有的类型检查错误都已修正,类型检查已经覆盖所有模块,基本上只需要把 require
改成 import
然后把代码和类型定义都放到 ts
文件中。完成之前的工作后,这一步相当简单。
把 jsconfig 改为 tsconfig
现在我们需要的是 tsconfig.json
而不是 jsconfig.json
。
tsconfig.json
的例子:
前端项目
后端项目
因为这样修改后类型检查会变得更严格,所以可能需要修复一些额外的类型错误。
修改 CI/CD 和构建流程
改到 TypeScript 后需要在构建流程中生成可运行的代码,通常在 package.json
中加上这一行就行:
不过,前端项目通常用了 babel,你需要这样设置项目:
别忘了改入口文件,比如:
好了,万事俱备。
注意,dist
需要改成你实际使用的目录。
结语
恭喜,代码现在迁移到了 TypeScript,有严格的类型检查保证。现在可以享受 TypeScript 带来的所有好处,比如自动补全、静态类型、esnext 语法、对大型项目友好。开发体验大大提升,维护成本大大降低。编写项目代码不再是痛苦的过程,再也不会碰到 Cannot read property 'x' of undefined
报错。
替代方案:
如果你希望一下子迁移整个项目到 TypeScript,可以参考 airbnb 团队的指南。
LeanCloud,领先的 BaaS 提供商,为移动开发提供强有力的后端支持。更多内容请关注「LeanCloud 通讯」
评论