我们是如何将3万代码从Flow移植到TypeScript的?

最近,我们将MemSQL Studio的3万行Flow代码移植到了TypeScript。在本文中,我将分享为什么我们要移植代码、我们是如何移植的,以及代码移植背后的原理。

免责声明:写这篇文章不是为了谴责Flow,我本人其实非常欣赏这个项目,我认为JavaScript社区为两种类型检查器的存在提供了足够的空间。只是每个团队都应该研究透所有的可选项,并选择最适合他们的方案。我希望这篇文章能够帮助你做出这样的选择。

先让我们从背景开始讲。在MemSQL,我们喜欢使用静态强类型的JavaScript,因为这样可以避免动态弱类型带来的常见问题,例如:

1.由于代码的不同部分使用了不一致的隐式类型契约,导致发生运行时类型错误。

2.需要花费太多时间来处理诸如参数类型检查之类的微不足道的事情(运行时类型检查增加了包的大小)。

3.缺乏编辑器和IDE集成,因为在没有静态类型的情况下,很难使用“跳转到定义”、机械式重构和其他功能。

4.围绕数据模型编写代码,也就是说,我们可以先设计数据类型,然后代码基本上可以自己“编写代码”。

这些只是静态类型的一些优点,我在最近的一篇有关Flow的文章中也描述了其他一些好处。

2016年初,我们开始在一个内部JavaScript项目中使用tcomb来确保运行时类型安全(免责声明:我不是该项目的一部分)。虽然运行时类型检查有时很有用,但它无法取代静态类型。因此,我们决定在2016年开始的另一个项目中使用Flow。当时,Flow是一个很好的选择,因为:

  • 由Facebook提供支持,Facebook在发展React和React社区方面做得非常出色(他们也使用Flow开发React)。

  • 我们没有必要采用一个全新的JavaScript开发生态系统。为了tsc(TypeScript编译器)而放弃Babel对我们来说有点可怕,因为这样并不会给我们带来在将来切换到Flow或其他类型检查器(显然这件事已经发生了)的灵活性。

  • 我们没有必要为整个代码库添加类型(我们想要在全盘添加类型之前先感受一下静态类型的JavaScript),我们只为一部分文件添加类型。请注意,Flow和TypeScript现在都允许你这么做。

  • TypeScript(当时)缺少Flow已经支持的一些基本功能,例如查找类型、泛型参数默认值等。

2017年末,我们开始在整个应用程序中实现完整的类型覆盖(所有这些都是用JavaScript编写的,前端和后端都在浏览器中运行)。我们决定使用Flow,因为我们有成功的经验。

不过,Babel 7(支持TypeScript)的发布引起了我的注意。这个版本意味着采用TypeScript并不一定要引入整个TypeScript生态系统,我们可以继续使用Babel。更重要的是,这意味着我们可以使用TypeScript作为类型检查器,而不是一门“语言”。

在我看来,将类型检查器分离是在JavaScript中实现静态(和强)类型的一种更优雅的方式,因为:

1.对ES5代码和类型检查进行关注点分离是一个好主意。这样可以减少类型检查器锁定,并加快开发速度(如果类型检查器因为某些原因更新较慢,并不会影响到代码发布)。

2.Babel提供了TypeScript所没有的插件和功能。例如,Babel允许你指定支持哪些浏览器,它将自动生成可以在这些浏览器上运行的代码。这实现起来非常复杂,有Babel的实现就够了,没有必要让社区在两个项目中实现同样的东西。

3.我喜欢将JavaScript作为一门编程语言(除了缺少静态类型),而且我不知道TypeScript还会存在多长时间,但我相信ECMAScript会一直存在。因此,我更喜欢使用JavaScript编写代码。(请注意,我总是说“使用Flow”或“使用TypeScript”,而不是“使用Flow编写”或“使用TypeScript编写”,是因为我总是将它们视为工具而不是编程语言)。

当然,这种方法有一些缺点:

1.理论上,TypeScript编译器可以根据类型执行包优化,但如果进行关注点分离,就会缺失这个特性。

2.当你使用更多的工具和开发依赖项时,项目配置会变得有点复杂。但我认为这并不是个大问题,因为在我们的项目中,Babel + Flow的配置从来都不是什么问题。

调研TypeScript作为Flow的替代方案

我注意到,在线和本地的JavaScript社区对TypeScript越来越感兴趣。因此,当我第一次发现Babel 7开始支持TypeScript时,我开始想办法抛弃Flow。最重要的是,在使用Flow时,我们遇到了各种挫败:

1.编辑器或IDE集成度低(与TypeScript相比)。

2.社区力量较弱,因此库的数量较少,而且库的类型定义质量不高(稍后将详细介绍)。

3.Facebook Flow团队与社区之间缺乏互动,而且没有公共路线图。

4.高内存消耗和频繁的内存泄漏——我们团队中的工程师见过Flow偶尔会占用近10 GB的内存。

当然,我们还必须研究TypeScript是否适合我们。这件事非常复杂,我们全面阅读了它的文档,终于搞清楚Flow中的每个功能在TypeScript中都有等效的功能。然后,我研究了TypeScript公共路线图,并对它提供的功能感到非常满意(例如,部分类型参数推断是我们在Flow中使用的一个特性)。

将3万行代码行从Flow移植到TypeScript

将所有代码从Flow移植到TypeScript的第一步是将Babel从6升级到7。这个其实很简单,但也花了我们大约2个人天,因为我们同时也将Webpack从3升级到了4。因为我们的源代码中存在一些遗留的依赖项,所以给升级过程带来了一点难度。

完成升级后,我们使用新的TypeScript预设替换Babel的Flow预设,然后运行TypeScript编译器,结果出现了8245个语法错误(tsc编译器只会在没有语法错误的情况下告诉我们真正的代码错误是什么)。

这个数字起初吓了我们一跳,但我们很快发现,其中大部分都与TypeScript不支持.js文件有关。经过一番调查,我发现TypeScript文件必须以“.ts”或“.tsx”结尾(如果文件中包含了JSX)。我在创建新文件时才不管是用哪个扩展名,所以我认为这是一个糟糕的开发者体验。为此,我不得不将每个文件的扩展名改为“.tsx”。

在更完扩展名之后,还有约4000个语法错误,大多数与导入类型有关,可以替换为TypeScript的“import”,也可以替换为Flow的对象表示法。在经过几次快速的正则表达式替换之后,语法错误降到了414个。剩下的都必须手动修复。

我们用于部分泛型类型参数推断的存在类型(existential type)必须被替换为显式命名的各种类型参数,或使用未知类型告诉TypeScript,我们不关心某些类型参数。
$Keys类型和其他Flow高级类型在TypeScript中具有不同的语法(例如,$Shape\u0026lt;\u0026gt;对应于TypeScript中的Partial\u0026lt;\u0026gt;)。

在修复了所有语法错误之后,tsc(TypeScript编译器)终于可以告诉我们代码库中有多少类型错误——大概有1300个。这个时候我们需要决定是继续下去,还是就此止步。毕竟,如果修复这些错误需要花费数周的时间,那么继续移植这些代码可能是不值得的。不过,我们认为实际的时间应该不会超过一个人周,于是我们决定继续。

请注意,在移植代码期间,我们不得不停止代码库上的其他工作。尽管我们可以继续添加新的代码,但前提是需要处理潜在的数百种类型错误,这不是一件容易的事。

这些类型错误都是些什么?

TypeScript和Flow对很多不同的东西做出了不同的假设,就是让JavaScript代码做不同的事情。Flow在某些方面更严格一些,而TypeScript则在其他方面更严格一些。要深入比较两种类型检查器之间的区别需要写很长的文章,在这篇文章中,我们只给出一些例子。

注意:本文假设所有代码都启用了“strict”模式。

invariant.js

在我们的源代码中,invariant是一个非常常见的函数。它的文档中已经解释得非常清楚,所以在这里我只是简单地引述:

var invariant = require('invariant');invariant(someTruthyVal, 'This will not throw');// No errorsinvariant(someFalseyVal, 'This will throw an error with this message');// Error raised: Invariant Violation: This will throw an error with this message

这个想法很简单——一个简单的函数,根据某些条件抛出错误。让我们来看看如何实现它并将其与Flow一起使用:

type Maybe\u0026lt;T\u0026gt; = T | void;function invariant(condition: boolean, message: string) {  if (!condition) {    throw new Error(message);  }}function f(x: Maybe\u0026lt;number\u0026gt;, c: number) {  if (c \u0026gt; 0) {    invariant(x !== undefined, \u0026quot;When c is positive, x should never be undefined\u0026quot;);    (x + 1); // works because x has been refined to \u0026quot;number\u0026quot;  }}

现在,让我们使用TypeScript运行完全相同的代码片段。我们会得到一个错误,因为它无法根据上一行代码知道“x”实际上已经不是undefined的。这是TypeScript的一个已知问题——它无法通过函数进行这种类型推断(尚不支持)。不过,因为这是我们代码库中非常常见的模式,我们不得不用手动代码替换invariant(超过150个地方):

type Maybe\u0026lt;T\u0026gt; = T | void;function f(x: Maybe\u0026lt;number\u0026gt;, c: number) {  if (c \u0026gt; 0) {    if (x === undefined) {      throw new Error(\u0026quot;When c is positive, x should never be undefined\u0026quot;);    }    (x + 1); // works because x has been refined to \u0026quot;number\u0026quot;  }}

虽然这不如使用invariant好,但也不是什么大问题。

$ExpectError与@ts-ignore

Flow有一个非常有趣的功能,类似于@ts-ignore,只是如果它的下一行代码不包含错误,它就会报错。这在编写“类型测试”时非常有用,这些测试确保类型检查器能够找到我们希望它们找到的某些类型错误。

不幸的是,TypeScript没有这个功能,这意味着我们的类型测试失去了一些价值。我很期待TypeScript能够实现这个功能

一般类型错误和类型推断

通常,Flow比TypeScript更加“聪明”一些。

type Leaf = {  host: string;  port: number;  type: \u0026quot;LEAF\u0026quot;;};type Aggregator = {  host: string;  port: number;  type: \u0026quot;AGGREGATOR\u0026quot;;}type MemsqlNode = Leaf | Aggregator;function f(leaves: Array\u0026lt;Leaf\u0026gt;, aggregators: Array\u0026lt;Aggregator\u0026gt;): Array\u0026lt;MemsqlNode\u0026gt; {  // The next line errors because you cannot concat aggregators to leaves.  return leaves.concat(aggregators);}

Flow将leaves.concat(aggregators)的类型推断为Array\u0026lt;Leaf | Aggregator\u0026gt;,然后可以转换为Array。我认为这是一个很好的例子,有时Flow更聪明一些,而TypeScript有时需要一些帮助(在这种情况下,我们可以使用类型断言来帮助TypeScript做出类型推断,但是使用类型断言有点危险,要十分小心)。

即使我这么说可能缺乏正式的证据支持,但我认为Flow在类型推断方面确实比TypeScript更优越一些。我非常希望TypeScript能够达到Flow的水平,TypeScript的开发很活跃,并且最近在这方面有了很多改进。在我们的大部分源代码中,我们必须通过注释或类型断言给TypeScript一些帮助(我们尽可能避免使用类型断言)。让我们再来看一个例子(我们可能有超过200个这种类型错误):

type Player = {    name: string;    age: number;    position: \u0026quot;STRIKER\u0026quot; | \u0026quot;GOALKEEPER\u0026quot;,};type F = () =\u0026gt; Promise\u0026lt;Array\u0026lt;Player\u0026gt;\u0026gt;;const f1: F = () =\u0026gt; {    return Promise.all([        {            name: \u0026quot;David Gomes\u0026quot;,            age: 23,            position: \u0026quot;GOALKEEPER\u0026quot;,        }, {            name: \u0026quot;Cristiano Ronaldo\u0026quot;,            age: 33,            position: \u0026quot;STRIKER\u0026quot;,        }    ]);};

TypeScript不会让你这么写,因为它不会让你把{ name: “David Gomes”, age: 23, type: “GOALKEEPER” }转换为Player类型的对象。这是另一个我认为TypeScript不够“聪明”的例子。

为了让这个奏效,你有几个选择:

  • 将“STRIKER”断言为“STRIKER”,这样TypeScript就知道这个字符串是\u0026quot;STRIKER\u0026quot; | \u0026quot;GOALKEEPER\u0026quot;类型的有效枚举。

  • 将整个对象断言为Player。

  • 或者使用Promise.all(…)代替断言,我认为是最好的解决方案。

另一个例子如下,再次说明Flow的类型推断比TypeScript更好:

type Connection = { id: number };declare function getConnection(): Connection;function resolveConnection() {  return new Promise(resolve =\u0026gt; {    return resolve(getConnection());  })}resolveConnection().then(conn =\u0026gt; {  // TypeScript errors in the next line because it does not understand  // that conn is of type Connection. We have to manually annotate  // resolveConnection as Promise\u0026lt;Connection\u0026gt;.  (conn.id);});

一个非常有趣的小例子是Flow将Array.pop()推断为T,而TypeScript认为它是T | void。在这一点上,TypeScript赢了,因为它会强制你检查该项是否存在(如果数组为空,Array.pop会返回undefined)。还有其他一些类似这样的小例子,在这些情况下,TypeScript比Flow更胜一筹。

用于第三方依赖项的TypeScript定义

在开发JavaScript应用程序时,你肯定会有一些第三方依赖项。它们也需要类型,否则就会失去静态类型的大部分功能(如本文开头所述)。

从npm导入的库会附带Flow类型定义或TypeScript类型定义,可能两者都会有,也可能都没有。(较小的)库不提供类型定义的情况也很常见,你必须为它们编写类型定义或者从社区中获取。Flow和TypeScript社区都提供了一个JavaScript第三方类型定义存储库:flow-typedDefinitelyTyped

我不得不说DefinitelyTyped让我们度过了更美好的时光。在使用flow-typed时,我们必须使用CLI工具将各种依赖项的类型定义引入到项目中。而DefinitelyTyped直接将这个功能合并到npm的CLI工具中,这样就可以更容易为依赖项引入类型定义(jest、react、lodash、react-redux,等等)。

除此之外,我也花了很多时间为DefinitelyTyped做贡献。我已经发出了几个拉取请求,只需要克隆代码,编辑类型定义,添加测试,最后发送拉取请求。DefinitelyTyped的GitHub机器人将自动标记被指定评审变更的人,如果他们在7天内没有进行评审,那么DefinitelyTyped的维护者将会评审拉取请求。合并到master后,依赖项的新版本将被发送到npm。

总体而言,DefinitelyTyped的类型定义要好一些,因为TypeScript背后的社区更强大、更繁荣。事实上,在将我们的项目从Flow移植到TypeScript之后,我们的类型覆盖率从88%增加到96%,这主要是因为有了更好的第三方依赖类型定义。

linting和测试

1.我们从eslint转向了tslint(我们发现在TypeScript中使用eslint会比较复杂,所以选择了tslint)。

2.我们使用ts-jest来运行TypeScript相关的测试。我们的一些测试是有类型的,有些是无类型的。

在修复了所有类型错误后还发生了什么?

经过一个人周的工作后,只剩下最后一个类型错误,我们暂时使用@ts-ignore忽略了它。

在处理了一些代码评审注释并修复了一些错误之后(我们不得不修改少量运行时代码来修复TypeScript无法理解的逻辑),我们开始创建拉取请求,并从那时起,我们就一直在使用TypeScript(我们在后续拉取请求中修复了最后一个@ts-ignore)。

除了编辑器集成之外,使用TypeScript与使用Flow非常相似。Flow服务器的性能稍微快一点,但这并不是一个大问题,因为它们为你正在查看的文件提供错误信息的速度是一样的。唯一的性能差异是TypeScript需要更长的时间(约0.5到1秒)来告诉你项目中是否有新的错误(在你保存文件后)。服务器启动时间大致相同(约2分钟),但这并不重要。到目前为止,我们没有遇到内存占用太高的问题,tsc似乎一直使用大约600 M的内存。

Flow的类型推断似乎比TypeScript好得多,但这不是个大问题,因为:

1.我们将整个使用Flow的代码库迁移成使用TypeScript。在这个过程中,我们肯定会发现一些Flow可以表达但TypeScript却做不到的东西。如果是反过来,我相信也会发现一些东西是TypeScript能够推断或表达但Flow却做不到的。

2.类型推断很重要,它有助于保持代码的简洁。但强大的社区和类型定义的可用性之类的东西更重要,因为弱类型推断可以通过更多的“手持”类型检查器来实现。

代码统计

\"\"

\"\"

下一步

我们的静态类型分析改进还没有结束。MemSQL还有其他项目,最终也会从Flow转向TypeScript。我们希望使我们的TypeScript配置更加严格。我们目前已启用“strictNullChecks”,但仍然禁用“noImplicitAny”。我们还打算移除一些危险的类型断言。

英文原文:https://davidgom.es/porting-30k-lines-of-code-from-flow-to-typescript/

更多内容,请关注前端之巅(ID:frontshow)

\"\"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值