将30,000行代码从Flow传输到TypeScript

最近,我们将MemSQL Studio系统中的30,000行JavaScript代码从Flow移到了TypeScript。 在本文中,我将解释为什么移植代码库,它是如何发生的以及发生了什么。

免责声明:我的目标是完全不批评Flow。 我很欣赏这个项目,并且认为JavaScript社区中有足够的空间来容纳两个类型检查选项。 最后,每个人都会选择最适合他的东西。 我衷心希望这篇文章对您的选择有所帮助。

首先,我将为您介绍最新信息。 我们在MemSQL上非常喜欢静态和强类型的JavaScript,以避免动态和弱类型的常见问题。

关于常见问题的演讲:

  1. 由于代码的不同部分与隐式类型不匹配,因此在运行时出现类型错误。
  2. 用于编写诸如检查类型参数之类的琐碎事情的测试花费了太多时间(在运行时中进行检查还会增加程序包的大小)。
  3. 缺少编辑器/ IDE集成,因为如果没有静态类型,则实现“跳转到定义”功能,机械重构和其他功能要困难得多。
  4. 无法围绕数据模型编写代码,即先设计数据类型,然后再由代码基本上“编写自身”。

这些只是静态类型化的一些好处, 有关Flow最新文章中进一步列出了这些好处。

在2016年初,我们实现了tcomb,以在我们内部JavaScript项目之一的运行时中实现某种类型的安全性(免责声明:我没有处理此项目)。 尽管运行时检查有时很有用,但它甚至不能提供静态类型的所有优点(运行时静态类型和类型检查的组合可能适用于某些情况, io-ts允许您使用tcomb和TypeScript进行此操作,尽管我从未尝试过) 了解了这一点后,我们决定为2016年开始的另一个项目实施Flow。 当时,Flow似乎是一个不错的选择:

  • Facebook的支持,这在开发React和发展社区方面做得非常出色(他们还开发了React with Flow)。
  • 大致相同的JavaScript开发生态系统。 放弃Babel来使用tsc(TypeScript编译器)很可怕,因为我们失去了切换到另一种类型检查的灵活性(显然,此后情况已经发生了变化)。
  • 无需代表整个代码库(我们想要在全部使用之前先获得静态类型的JavaScript的想法),而只需要部分文件即可。 请注意,Flow和TypeScript现在都允许这样做。
  • TypeScript(当时)缺少一些现在可用的基本功能,这些功能是查找类型泛型类型的默认参数等。

当我们在2017年底开始研究MemSQL Studio时,我们将介绍整个应用程序的类型(它完全是用JavaScript编写的:前端和后端都在浏览器中执行)。 我们将Flow作为过去成功使用的工具。

但我的注意力吸引到了具有TypeScript支持的Babel 7 。 此版本意味着切换到TypeScript不再需要过渡到整个TypeScript生态系统,并且您可以继续将Babel用于JavaScript。 更重要的是,我们只能将TypeScript用于类型检查 ,而不能将其用作完整的“语言”。

我个人认为,从代码生成器中分离类型检查是在JavaScript中进行静态(且强大)键入的一种更优雅的方法,因为:

  1. 我们分享代码和打字的问题。 这样可以减少类型检查的次数并加快开发速度:如果由于某种原因类型检查很慢,代码仍将正确生成(如果将tsc与Babel结合使用,则可以对其进行配置)。
  2. Babel具有TypeScript生成器所没有的出色插件和功能。 例如,Babel允许您指定受支持的浏览器,并将自动为其发布代码。 这是一个非常复杂的功能,没有必要在两个不同的项目中同时支持它。
  3. 我喜欢JavaScript作为一种编程语言(缺少静态类型除外),并且我不知道有多少TypeScript,尽管我相信ECMAScript已有很多年了。 因此,我更喜欢用JavaScript编写和“思考”(请注意,我说“使用Flow”或“使用TypeScript”而不是“在Flow中编写”或“ TypeScript”,因为我总是用工具而不是编程语言来表示它们)。

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

  1. 理论上,TypeScript编译器可以执行基于类型的优化,但是在这里我们失去了机会。
  2. 随着工具和依赖项数量的增加,项目的配置稍微复杂一些。 我认为这是一个相对较弱的论点:一堆Babel和Flow从来没有让我们失望过。

TypeScript替代Flow


我注意到JavaScript社区对TypeScript的兴趣日益增长:在线以及周围的开发人员中都如此。 因此,一旦我发现Babel 7支持TypeScript,便立即开始研究潜在的转换选项。 另外,我们遇到了Flow的一些缺点:

  1. 编辑器/ IDE集成的质量较低(与TypeScript相比)。 Facebook自己的IDE集成度最高的Nuclide已经过时了。
  2. 较小的社区,这意味着不同库的类型定义较少,并且它们的质量较低(当前DefinitelyTyped存储库具有19 682个GitHub star,而流类型存储库只有3070个)。
  3. 缺乏公共发展计划,Facebook上的Flow团队与社区之间缺乏互动。 您可以从Facebook员工那里阅读此评论以了解情况。
  4. 高内存消耗和频繁泄漏-对于我们的某些开发人员,Flow有时会占用近10 GB的RAM。

当然,您应该研究TypeScript如何适合我们。 这是一个非常复杂的问题:研究主题包括对文档的透彻阅读,这有助于了解每个Flow函数都有一个等效的TypeScript。 然后,我探索了TypeScript公共开发计划,并且我真的很喜欢为将来计划的功能(例如,部分衍生我们在Flow中使用的类型参数)。

将超过3万行代码从Flow传输到TypeScript


首先,您应该将Babel从6升级到7。这个简单的任务花了16个工时,因为我们决定同时将Webpack 3升级到4,因为一些过时的依赖使我们的代码变得复杂。 绝大多数JavaScript项目都不会出现此类问题。

之后,我们将Babel Flow预设替换为新的TypeScript预设,然后首次在所有使用Flow编写的源代码上启动TypeScript编译器。 结果是8245语法错误 (在修复所有语法错误之前,tsc CLI不会显示项目的实际错误)。

起初,这个数字使我们非常害怕,但我们很快意识到大多数错误是由于TypeScript不支持.js文件引起的。 研究了该主题之后,我了解到TypeScript文件应以.ts或.tsx结尾(如果它们具有JSX)。 在我看来,这显然带来了不便。 为了不考虑是否存在JSX,我只是将所有文件重命名为.tsx。

仍然存在大约4,000个语法错误。 它们大多数与类型导入有关, 类型导入可以用类型脚本简单地替换为导入,以及对象指定的区别( {||}代替{} )。 快速应用几个正则表达式,我们留下了414个语法错误。 其他所有内容都必须手动修复:

  • 我们用于部分派生泛型类型参数的存在类型应替换为显式参数或未知类型,以告知TypeScript一些参数不重要。
  • 类型$键和其他高级Flow类型在TypeScript中具有不同的语法(例如, $Shape“”对应于TypeScript中的Partial“” )。

纠正了所有语法错误之后,tsc最终说出我们的代码库中有多少个实类型错误只有大约1300个。现在我们不得不坐下来决定是否继续。 毕竟,如果迁移需要花费数周的时间,则最好继续使用Flow。 但是,我们认为代码移植只需要一名工程师不到一周的时间,这是完全可以接受的。

请注意,在迁移过程中,我必须停止此代码库上的所有工作。 然而,并行地,您可以启动新项目-但您必须牢记现有代码中潜在的数百种类型错误,这并不容易。

什么样的错误?


TypeScript和Flow以多种方式处理JavaScript代码。 因此,对于某些事物,Flow更为严格;对于其他事物,TypeScript更为严格。 对这两个系统的深入比较将非常漫长,因此,请看一些示例。

注意:指向TypeScript沙箱的所有链接均采用“严格”参数。 不幸的是,当您共享链接时,这些选项未存储在URL中。 因此,在打开本文中指向沙箱的任何链接后,必须手动设置它们。

invariant.js


invariant函数在我们的源代码中非常常见。 仅引用文档:

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

这个想法很明确:一个简单的函数会在某些情况下引发错误。 让我们看看如何在Flow上实现和使用它

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

现在,在TypeScript中加载相同的代码段 。 正如您从链接中看到的那样,TypeScript给出了一个错误,因为它无法理解保证x在最后一行之后不会保持undefined 。 这实际上是一个众所周知的问题 -TypeScript(目前)不知道如何通过函数进行此推断。 但是,这是我们代码库中非常常见的模板,因此我不得不用另一个立即产生错误的代码手动替换每个不变实例(超过150个)。

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

不能真正地与invariant相比,但不是那么重要的问题。

$ ExpectError vs @ ts-ignore


Flow具有一个非常有趣的功能,类似于@ts-ignore ,不同之处在于如果下一行不是错误,它将抛出错误。 这对于编写确保类型检查(无论TypeScript还是Flow)发现某些类型错误的“类型测试”非常有用。

不幸的是,TypeScript没有这样的功能,因此我们的测试失去了一些价值。 我期待在TypeScript上实现此功能

通用类型错误和类型推断


通常,TypeScript比Flow允许使用更明确的代码,如以下示例所示:

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

Flow 推断leaves.concat(聚合器)的类型为Array <Leaf | Aggregator> ,然后可以Array<MemsqlNode>Array<MemsqlNode>Array<MemsqlNode> 。 我认为这是一个很好的示例,其中Flow可以更智能一些,而TypeScript需要一些帮助:在这种情况下,我们可以应用类型断言,但这很危险,应该非常小心地进行。

尽管我没有正式的证据,但我认为Flow在类型推断方面远远优于TypeScript。 我真的希望TypeScript能够达到Flow的水平,因为该语言正在非常积极地发展,并且在此领域已经取得了许多新的改进。 在我们代码的许多地方,TypeScript不得不通过注释或类型断言来提供一些帮助,尽管我们尽可能避免使用后者 。 让我们再考虑一个示例 (我们有200多个此类错误):

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

TypeScript不允许您编写此代码,因为它不允许您将{ name: "David Gomes", age: 23, type: "GOALKEEPER" }Player类型的对象(确切的错误请参见沙盒)。 这是我发现TypeScript不够聪明的另一种情况(至少与理解此代码的Flow相比)。

有几种解决方法:

  • 声明"STRIKER" "STRIKER"以便TypeScript理解该字符串是"STRIKER" | "GOALKEEPER"类型的有效枚举。 "STRIKER" | "GOALKEEPER"
  • 将所有对象声明为Player
  • 或者我认为是最好的解决方案:通过编写Promise.all<Player>(...)来帮助TypeScript而不使用任何类型语句。

这是另一个示例 (TypeScript),其中Flow再次在类型推断方面更好

 type Connection = { id: number }; declare function getConnection(): Connection; function resolveConnection() { return new Promise(resolve => { return resolve(getConnection()); }) } resolveConnection().then(conn => { // 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<Connection>. (conn.id); }); 

一个非常小的但有趣的示例:Flow认为Array<T>.pop() T类型,而TypeScript则将其视为T | void T | void 支持TypeScript的观点是因为它迫使您仔细检查元素的存在(如果数组为空,则Array.pop返回undefined )。 像这样的其他几个小示例,其中TypeScript优于Flow。

第三方依赖项的TypeScript定义


当然,在编写任何JavaScript应用程序时,您将至少具有一些依赖性。 应该键入它们,否则您将失去静态类型分析的大多数可能性(如本文开头所述)。

来自npm的库可以带有Flow或TypeScript类型定义,可以有或没有。 通常,(小型)库中的任何一个都不提供,因此您必须编写自己的类型定义或从社区借用它们。 Flow和TypeScript都支持第三方JavaScript软件包的标准定义存储库:这些存储库是flow-typedDefinitelyTyped

我必须说DefinitelyTyped我们更​​喜欢。 对于流类型,我必须使用CLI工具将项目中各种依赖项的类型定义引入项目中。 DefinitelyTyped通过将@types/package-name软件包发送到npm软件包存储库,将此功能与npm CLI工具结合在一起。 这非常酷,大大简化了我们依赖项的类型定义的输入(Jest,react,lodash,react-redux,这些只是少数)。

另外,我花了很多时间来填充DefinitelyTyped数据库(将代码从Flow移植到TypeScript时,不要认为类型定义是等效的)。 我已经 发送了 一些 请求 ,并且在任何地方都没有问题。 只需克隆存储库,编辑类型定义,添加测试-并发送拉取请求。 DefinitelyTyped GitHub机器人标记了您编辑的定义的作者。 如果他们在7天内均未提供反馈,则将拉取请求提交给维护者考虑。 与main分支合并后,新版本的依赖项程序包将发送到npm。 例如,当我第一次更新@ types / redux-form软件包时,版本7.4.14被自动发送到npm。 因此只需更新package.json文件即可获取新的类型定义。 如果您迫不及待希望通过请求请求,则可以随时更改项目中使用的类型的定义,如前一篇文章所述

通常,由于拥有更大,更繁荣的TypeScript社区,所以DefinitelyTyped中类型定义的质量要好得多。 实际上,在将项目转移到TypeScript之后,我们的类型覆盖率从88%增加到96% ,这主要是由于更好地定义了第三方依赖关系类型,而更少了any类型。

整理和测试


  1. 我们从eslint切换到tslint (对于TypeScript使用eslint,它似乎更难入门)。
  2. TypeScript测试使用ts-jest 。 有些测试是键入的,而有些则没有(如果键入的时间太长,我们会将它们另存为.js文件)。

解决所有输入错误后会发生什么?


工作40个小时后,我们遇到了最后一个键入错误,使用@ts-ignore将其推迟了一段时间。

在审查了代码审查注释并修复了两个错误之后(不幸的是,我不得不稍稍更改运行时代码以修复TypeScript无法理解的逻辑),拉取请求消失了,此后我们一直在使用TypeScript。 (是的,我们在下一个拉取请求中修复了最后一个@ts-ignore )。

除了与编辑器集成外,使用TypeScript与使用Flow非常相似。 流服务器的性能略高,但这不是一个大问题,因为它们会同样快地为当前文件生成错误。 唯一的性能差异是TypeScript在稍后保存文件(0.5-1 s)后报告新错误。 服务器启动时间大致相同(约2分钟),但并不是很重要。 到目前为止,我们在内存消耗方面还没有任何问题。 好像tsc经常使用大约600 MB。

似乎类型推断函数为Flow提供了很大的优势,但是有两个原因使它实际上并不重要:

  1. 我们将Flow代码库转换为TypeScript。 显然,我们只遇到了Flow可以表达的代码,而TypeScript没有。 如果迁移的方向相反,我相信TypeScript可以更好地显示/表达某些内容。
  2. 类型推断对于帮助编写更简洁的代码很重要。 但是,其他所有事情同样重要,例如强大的社区和类型定义的可用性,因为弱类型推断可以通过花费更多时间键入而得到解决。

代码统计


 $ npm run type-coverage # https://github.com/plantain-00/type-coverage 43330 / 45047 96.19% $ cloc # ignoring tests and dependencies -------------------------------------------------------------------------------- Language files blank comment code -------------------------------------------------------------------------------- TypeScript 330 5179 1405 31463 

接下来是什么?


我们还没有完成改进静态类型分析的工作。 MemSQL还有其他项目,这些项目最终将从Flow切换到TypeScript(以及一些将开始使用TypeScript的JavaScript项目),我们希望使我们的TypeScript配置更加严格。 当前,我们已启用strictNullChecks选项,但noImplicitAny仍被禁用。 我们还将从代码中删除几个危险的类型语句

我很高兴与您分享我在输入JavaScript的过程中所学到的一切。 如果您对特定主题感兴趣,请告诉我

Source: https://habr.com/ru/post/zh-CN436554/


All Articles