来自 Lyft 的前端工程师 Mohsen Azimi 介绍了 Lyft 向 TypeScript 转型的过程,说明 JavaScript 类型系统的重要性、为什么 Lyft 选择 TypeScript 以及他们的一些实践经验。
以下内容翻译自作者的博客,查看原文:
https://eng.lyft.com/typescript-at-lyft-64f0702346ea
在我刚刚成为 JavaScript 开发者的时候,当有人说要给 JavaScript 加入类型系统,我就会问自己:为什么要这么做?
现在,我已经成为一个 JavaScript 老手,我难以想象没有类型系统支持的 JavaScript 会是怎样的。大型的 JavaScript 应用需要类型信息来提升扩展性和可维护性,Lyft 的很多 JavaScript 项目(从 Lyft.com 网站到我们的内部工具)也不例外。当我还是个 JavaScript 狂热者的时候,看着 Lyft 的团队和代码库在膨胀,开始意识到存粹的 JavaScript 已经无法支撑起大型的应用了。
但这也并非意味着要扔掉 JavaScript。如果能够加入类型系统,一切都会变得不一样。类型系统可以减少 bug,开发者也因此能够更加方便地查看代码。下面我将讲述 Lyft 为什么选择了 TypeScript 以及是怎么做到的。
“Uncaught TypeError: Cannot read property "foo" of undefined”是 JavaScript 最常见的一个错误。在访问一个未定义(undefined)的引用对象的属性时就会发生这个错误。Lyft 的纯 JavaScript 项目在生产环境经常会出现这个问题。JavaScript 的常见错误还包括拼写错误,比如“document.getElementbyId”,这类错误在生产环境会引发大问题。
REST API 的类型不匹配问题虽然不常见,但一旦发生就是个大 bug。比如,API 响应消息里的一个字段从数字类型变成字符串类型,会导致 JavaScript 出现不可预期的行为。
浏览大型的 JavaScript 代码库不仅耗时而且容易让人感到困惑,找不到函数的定义或搞不清楚函数可以接收哪些参数都是常有的事。以下列这段代码为例:
/**
* Create an input element
* @param {string} type
* @param {string|boolean} value
* @return {HTMLInputElement}
*/
function createInput(type, value) {
const el = document.createElement('input');
el.type = type;
if (type === 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
return el;
}
这个函数根据传入的类型返回一个 HTMLInputElement 对象,如果是复选框类型,就把复选框的“checked”属性值设置为传入的值,否则的话就创建一个输入框,并把输入框的值设置为传入的值。
这里可能会出现 bug,假设在创建复选框时传入了错误的值,比如:
const input = createInput('checkbox', 'false')
即使代码注释里写得很清楚,仍然无法阻止 bug 的产生。把字符串“false”赋值给复选框,但复选框的状态仍然会是“true”,因为字符串“false”会被解析成布尔类型的“true”。
而如果有了类型系统,就可以通过重载帮助开发人员写出正确的代码:
/**
* Create an input element
*/
function createInput(type: 'text', value: string): HTMLInputElement;
function createInput(type: 'checkbox', value: boolean): HTMLInputElement;
function createInput(type, value) {
// code
}
类型系统让代码变得更强大,通过 API 级别的语义可以在一开始就把 bug 扼杀在襁褓里。
类型系统让重构变得更简单,开发人员也因此可以放心地做出代码变更。例如,当一个函数签名发生变化,在调用方代码没有做出相应改动之前是无法通过 TypeScript 编译的。
强类型的代码之所以更容易进行重构,是因为类型检查器可以确保代码变更可以与项目的其他部分兼容。IDE 或代码编辑器为类型系统提供了支持。
带有类型信息的模块更容易维护。使用 TypeScript 开发的模块在一些编辑器里可以显示出 API 的提示信息。
TypeScript 与 FlowType 的对决
我们有多种 JavaScript 类型系统可选择:
虽然我们最终选择了 TypeScript,但做出这个决定也并不是那么容易的。我们的团队分成两个阵营,一个倾向于选择 FlowType,一个倾向于选择 TypeScript。
FlowType 被认为“就是 JavaScript”或者“带有类型注解的 JavaScript”。这种看法有失偏颇。FlowType 是一门独立的语言,它的语法是 JavaScript 的超集。它使用了.js 作为文件扩展名,所以导致了人们的混淆。实际上,不管是 JSX 还是 FlowType,它们都不是 JavaScript。同样,TypeScript 和 ECMAScript 也不是。
调用方类型检查是 FlowType 的一个非常受欢迎的特性。比如:
function power2(a) {
return a * a;
}
power2('string')
这个函数接收一个数字类型的参数,如果在调用时传入了一个字符串,FlowType 会对此作出警告。而 TypeScript 则会认为“a”是任意类型,所以可以通过编译。
这个特性看起来令人印象深刻,但我们只要对代码稍作改动,这个特性就不管用了。比如:
function foo(a) {
console.log(a.b)
}
foo({})
这段代码可以通过 FlowType 的编译。
因为 React 和 FlowType 都是由 Facebook 开源的,看似 FlowType 比 TypeScript 更适合用在 React 中。但在 Lyft 的项目中,我们并没有发现把这两者用在 React 中有什么不同。
虽说 FlowType 和 TypeScript 看似旗鼓相当,但出于对生态系统未来发展的考虑,我们需要关注它们的流行程度。选择流行程度较高的那一个可以帮助 Lyft 吸引到更多的开发人才。
要衡量一个开源项目的流行程度并非易事,不过我还是试着努力找出它们之间的对比数据。
-
StackOverflow 上的问题数量
:FlowType——900 多个;TypeScript——38,000 多个。
-
GitHub 上的问题数量
:FlowType——1500 多个未解决,2200 个已关闭;TypeScript——2400 多个未解决,11,200 个已关闭。
-
GitHub 上的拉取请求
:FlowType——60 多个未解决,1,200 个已关闭;TypeScript——100 多个为解决,5000 多个已关闭。
-
npm 每月下载数量
:FlowType——290 多万次;TypeScript——720 多万次。
-
外部类型定义数量
:FlowType——340 多个,在 GitHub 上有 43000 个“流类型”目录,有些库还提供了.flow 类型定义;TypeScript——3700 多个,在 GitHub 上有约 25 万个 package.json 里包含了类型定义,Facebook 的 Redux 和 ImmutableJS 也提供了 TypeScript 类型定义。
我们在内部进行了一个问卷调查,与上述的数据一样,TypeScript 在 Lyft 内部也很受欢迎。
我们的项目里有大量的纯 JavaScript 代码,要一下子把它们全部转成 TypeScript 并非明智的做法。
于是,我们选择了增量迁移。我们使用 Webpack 编译我们的前端应用,通过 TypeScript-loader 可以很轻松地将 TypeScript 引入到 Webpack 中。有了 TypeScript-loader,我们就可以一边使用 TypeScript 编写新代码,一边零碎地更新旧代码。
TypeScript 编译器可以对 JavaScript 文件进行类型检查,所以我们就利用了这一特性对已有的 JavaScript 代码进行类型错误检查。当然,这个只对直接被导入到 TypeScript 中的 JavaScript 文件有效。不过,最新版本(2.5)的编译器几乎可以直接用于检查独立的 JavaScript 文件。
网上有很多 TypeScript 的学习资源。TypeScript 的官方网站就提供了大量的学习资料,方便开发者入门。另外,TypeScript 的语法是 JavaScript 的超集,所以对于前端开发人员来说非常直观。
lint 工具不仅有助于开发者学习 TypeScript,对写出一致、流畅的代码也很有帮助。lint 工具在没有类型系统的情况下有助于减少有问题的代码,而在有类型系统的情况下就更是能够起到保护代码的作用,所以我们在所有的项目里使用了 TSLint。TSLint 比一般的 lint 工具更强大,它可以捕捉到一些很意思的问题,比如 awaiting-noon-promise,而这在纯 JavaScript 的 lint 工具里是做不到的。
在进行 TypeScript 培训和往我们的代码库引入 TypeScript 的过程中,我们发现我们的很多 JavaScript 代码无法直接使用类型。虽然我们因此感到沮丧,但这也正好说明了我们的代码写得不好,需要进行重构。