
JavaScript是动态类型化的语言之一。 这种语言对于快速的应用程序开发很方便,但是当几个团队承担一个大项目的开发时,最好从一开始就选择一种类型检查工具。
您可以开始开发TypeScript代码或将其包含在Flow项目中。 TypeScript是Microsoft开发的JavaScript的编译版本。 与TypeScript不同,Flow不是一种语言,而是一种允许您分析代码和检查类型的工具。 您可以在网上找到许多有关这些方法的文章和视频,以及有关如何开始使用打字的指南。 在本文中,我们想告诉您Flow为什么不适合我们,以及我们如何开始切换到Typescript。
一点历史
在2016年,我们开始为ECM系统开发基于React / Redux的Web客户端。 选择Flow来测试打字的原因如下:
- React和Flow是同一家公司Facebook的产品。
- 流量发展更加积极。
- 流很容易集成到项目中。
但是随着项目的发展,开发团队的数量增加,使用Flow时出现了许多问题:
- 后台类型检查Flow使用了过多的PC资源。 结果,一些开发人员将其关闭,并根据需要开始检查。
- 在某些情况下,使代码与Flow一致需要花费很多时间来编写代码本身。
- 该代码开始出现在项目中,只有通过Flow测试才需要。 例如,仔细检查是否为空:
foo() { if (this.activeFormContainer == null) { return; }
- 大多数开发人员使用Visual Studio Code代码编辑器,其中Flow没有TypeScript那样好的支持。 自动完成(IntelliSense)在开发过程中并不总是起作用,并且代码导航不稳定。 我希望有和在Visual Studio中用C#编写时一样的开发便利。
一些开发人员的想法是尝试切换到TypeScript。 为了测试过渡的想法并说服管理层,我们决定尝试该原型。
样机
我们想测试两个关于原型的想法:
- 尝试翻译整个项目。
- 设置项目,以便您可以同时使用Flow和Typescript。
对于第一个想法,需要一个实用程序来转换所有项目文件。 在网络上找到其中之一。 从描述来看,她可以翻译最多的内容,但是某些更改必须由我们自己编辑,或者应添加实用程序本身。 我们能够用少量文件来转换测试项目。 但是实际项目无法编译,因此有必要编辑太多文件。 我们决定不继续朝这个方向发展,因为:
- 还有很多事情要做! 在我们完成该项目的同时,其余团队将继续开发新功能,修复错误,编写测试。 此外,合并文件将花费大量时间。
- 即使我们以这种方式翻译了项目,测试人员也将需要做多少工作!
尽管我们放弃了此选项,但我们获得了有益的经验。 很明显,翻译每个文件需要完成大约的工作量。 这是一个简单的React组件的翻译结果

如您所见,没有太多更改。 基本上,它们如下:
- 删除// @流;
- 用更熟悉的界面替换类型;
- 添加访问修饰符;
- 用ts库中的类型替换类型(来自图片示例:事件处理程序和事件本身)。
第二个想法的实现将允许进一步开发,但已经在TypeScript上进行开发,并且可以在后台缓慢地转换现有代码库。 这提供了几个优点:
- 易于翻译,无需担心丢失任何内容。
- 易于测试。
- 易于合并更改。
但是尚不清楚是否可以将项目配置为可以同时使用两种类型的打字。 在Internet上进行搜索并没有得到任何具体的结果,因此他们开始自己进行排序。 理论上,流分析器仅检查具有js / jsx扩展名并包含注释的文件:
对于TypeScript编译器,文件必须具有ts / tsx扩展名。 由此可知,两种键入方法都应同时起作用,并且不会相互干扰。 基于此,我们建立了项目环境。 利用第一个原型的经验,我们翻译了几个文件。 编译项目,启动客户端-一切都像以前一样工作!
绿灯
良好的一天-sprint计划的一天,我们的团队在积压的工作中有一个用户故事“开始切换到TypeScript”,其中包含以下工作清单:
- 设置webpack。
- 配置tslint。
- 设置测试环境。
- 将文件转换为TypeScript。
Webpack设置
第一步是教webpack如何处理ts / tsx扩展名的文件。 为此,我们在配置文件的“规则”部分添加了一条规则。 最初使用的ts-loader:
为了加快组装速度,关闭了类型检查:
transpileOnly: true
,因为 IDE在编写代码时已经指示错误。
但是,当我们开始转换Redux动作时,很明显,它们需要
babel-plugin-transform-class-display-name插件才能起作用。 该插件将静态displayName属性添加到所有类。 翻译后,仅处理了ts-loader动作,这不允许将babel插件应用于它们。 结果,我们放弃了ts-loader,并通过添加
babel / preset-typescript扩展了js / jsx的现有规则:
为了使TypeScript编译器正常工作,您需要添加tsconfig.json配置文件,该文件取自文档。
配置Tslint
使用eslint额外检查了使用Flow编写的代码。 TypeScript有其对应的tslint。 最初,我想将所有规则从eslint转移到tslint。 尝试通过tslint-eslint-rules插件同步规则,但是不支持大多数规则。 也可以使用eslint通过typescript-eslint-parser检查ts文件。 但是,不幸的是,只有一个解析器可以连接到eslint。 如果仅对所有类型的文件使用ts-parser,则js文件和ts中都会出现很多奇怪的错误。 因此,我们使用了建议的规则集,并扩展到了我们的要求:
// tslint.json "extends": ["tslint:recommended", "tslint-react"]
将文件转换成TypeScript
现在一切就绪,您可以开始翻译文件了。 首先,我们决定转移一个小的React组件,该组件将在整个项目中使用。 选择取决于“按钮”组件。

在翻译过程中,我们遇到了一个问题:并非所有第三方库都具有TypeScript类型,例如bem-cn-lite。 在Microsoft的
TypeSearch资源上,找不到它的类型库。 对于几乎所有必需的库,我们找到并连接了ts类型库。 一种解决方案是通过require连接:
const b = require('bem-cn-lite');
但是同时,类型不足的问题也没有解决。 因此,我们使用
dts-gen实用程序为类型自己生成了一个“存根”:
dts-gen -m bem-cn-lite
该实用程序生成了扩展名为* .d.ts的文件。 该文件放置在@types文件夹中,并配置了tsconfig.json:
// tsconfig.json "typeRoots": [ "./@types", "./node_modules/@types" ]
接下来,类似于原型,我们翻译了组件。 编译了项目,启动了客户端-一切正常! 但是测试失败了。
测试环境设置
为了测试该应用程序,我们使用Storybook和Mocha。
故事书用于视觉回归测试(
文章 )。 与项目本身一样,它是使用webpack构建的,并具有自己的配置文件。 因此,要使用ts / tsx文件,必须类似于项目本身的配置进行配置。
当我们使用ts-loader构建项目时,我们停止运行Mocha测试。 要解决此问题,请将ts-node添加到测试环境:
// mocha.opts --require @babel/polyfill --require @babel/register --require test/index.js --require tsconfig-paths/register --require ts-node/register/transpile-only --recursive --reporter mochawesome --reporter-options reportDir=../../bin/TestResults,reportName=js-test-results,inlineAssets=true --exit
但是切换到Babel后,您可以摆脱它。
问题所在
在翻译过程中,我们遇到了许多程度不同的问题。 它们主要与我们对TypeScript缺乏经验有关。 以下是其中一些:
- 从不同的文件类型导入组件/功能。
- 高阶组件的翻译。
- 丢失更改历史记录。
从不同的文件类型导入组件/功能
当使用不同文件类型的组件/功能时,有必要指定文件扩展名:
import { foo } from './utils.ts'
这允许您将有效扩展名添加到webpack和eslint配置文件:
高阶成分的翻译
在所有文件类型中,高阶组件(HOC)的翻译引起最多的问题。 这是一个在输入中使用组件并返回新组件的函数。 它主要用于重用逻辑,例如,它可以是添加选择元素功能的函数:
const MyComponentWithSeletedItem = withSelectedItem(MyComponent);
或最著名的连接,来自Redux库。 键入此类函数并非易事,需要连接其他库以使用类型。 我不会详细介绍翻译过程,因为您可以在网上找到许多有关该主题的手册。 简而言之,问题在于这样的函数是抽象的:具有任何属性集的任何组件都可以接受输入。 它可以是具有title和onClick属性的Button组件,也可以是具有alt和imgUrl属性的Picture组件。 我们事先不知道这些属性的集合;只有函数本身添加的那些属性是已知的。 为了使TypeScript编译器在使用通过此类函数获得的组件时不会发誓,有必要从返回类型中“剪切”该函数添加的属性。
为此,您需要:
- 将这些属性拉入接口:
interface IWithSelectItem { selectedItem: number; handleSelectedItemChange: (id: number) => void; }
- 从组件接口中删除所有进入IWithSelectItem接口的属性。 为此,您可以使用实用程序类型库中的Diff <T,U>操作。
React.ComponentType<Diff<TPropsComponent, IWithSelectItem>>
丢失更改历史记录
要使用源代码(例如代码审查),我们使用Team Foundation Server。 翻译文件时,我们遇到了一项不愉快的功能。 在请求池中将显示两个文件,而不是一个更改的文件:
- remote-文件的旧版本;
- 已创建-新版本。

如果文件中有很多更改(相似性<50%),例如对于小文件,就会观察到此行为。 为了解决这个问题,我们尝试使用:
- git mv命令
- 执行两次提交:第一个提交更改文件扩展名,第二个提交立即更正。
但是,不幸的是,这两种方法都无济于事。
总结
使用Flow或TypeScript-每个人都可以自己决定,两种方法各有利弊。 我们为自己选择了TypeScript。 而且,您从自己的经验中获得了说服力:如果您选择了其中一种方法并突然意识到,即使三年后它仍然不适合您,那么您可以随时进行更改。 为了使过渡更加顺利,您可以像我们一样配置项目以使其并行工作。
在撰写本文时,我们还没有完全切换到TypeScript,但是我们已经重写了主要部分-项目的“核心”。 在代码库中,您可以找到从简单的react组件到高阶组件的各种文件转换的示例。 而且,在所有开发团队之间进行了培训,现在每个团队作为其任务的一部分,将项目的一部分转移到这些职责上。
我们计划在年底之前完成过渡,翻译测试和故事书,甚至编写一些tslint规则。
根据我个人的感觉,我可以说开发开始花费的时间越来越少,类型检查是在不加载系统的情况下进行的,而对我来说错误消息变得更加易于理解。