您是否经常发现自己在某些代码中排查错误时,却发现错误是一些本可以避免的简单错误? 也许您将参数以错误的顺序传递给了函数,或者您试图传递的是字符串而不是数字?JavaScript是一种弱类型语言, 强制进行变量转换可能会导致不可某些预知的错误。

Flow是一个JavaScript的静态类型检查器,最初由Facebook在2014年的Scale大会上提出。它的目标是在JavaScript代码中发现类型错误,通常不需要修改源代码。同时,它还向JavaScript添加了新的语法,为开发人员提供了更多的控制。

在本文中,我将向您介绍Flow及其主要特性。我们将了解如何设置它,如何向代码中添加类型注释,以及如何在运行代码时自动删除这些注释。

安装

最简单的方法是通过安装npm:

npm install --save-dev flow-bin

package.json增加scripts脚本:

"scripts": {
  "flow": "flow"
}

下面我们将探索Flow的功能。

开始

在项目根目录下创建一个名为.flowconfig的配置文件。可以通过运行以下命令来创建一个空的配置文件:

npm run flow init

一旦配置文件创建成功,您可以在终端上运行以下命令,对项目文件夹和任何子文件夹中的代码进行代码检查:

npm run flow check

这并不是使用Flow最有效的方法,因为它会导致Flow本身每次都要重新检查整个项目。我们可以使用Flow server代替。

Flow server采取增量检查文件的方式,它只检查已更改的部分。服务器可以通过在终端上运行命令npm run flow启动。

第一次运行此命令时,服务器将启动并显示初始测试结果。这使得工作流程更加快速和增量。每次您想知道测试结果时,在终端上运行flow。在完成编码之后,可以使用npm run flow stop停止服务器。

Flow的类型检查是可选的。您不需要一次检查所有代码。您可以选择需要Flow检查的文件。在任何JavaScript文件的顶部添加@flow注释:

/*@flow*/

将Flow集成到现有项目中时,这会非常有用,因为您可以选择要逐个检查的文件并解决任何错误。

Type 检查

一般来说,类型检查有两种方法:

  • 注解:我们指定我们期望的类型作为代码的一部分,类型检查器根据这些期望来评估代码
  • 通过代码:Flow足够智能,可以通过查看使用变量的上下文来推断预期的类型,并根据上下文检查代码

使用注解的方式,需要编写一些额外的代码,这些代码只在开发过程中有用,并且是从浏览器加载的最终JavaScript构建中剥离出来的。这需要一些额外的前期工作,通过添加这些额外的类型注释来检查代码。

在第二种情况下,代码已经准备好无需任何修改即可进行测试,从而最大限度地减少了开发量。它不会强制您更改编码方式,因为它会自动推断表达式的数据类型。这称为类型推断,是Flow最重要的特性之一。

为了说明这个特性,我们可以以下面的代码为例:

/*@flow*/

function foo(x) {
  return x.split(' ');
}

foo(34);

当您运行npm run flow命令时,这段代码将在终端上给出一个错误,因为函数foo()在传递一个数字作为参数时希望得到一个字符串。

错误是这样的:

index.js:4
  4:   return x.split(' ');
                ^^^^^ property `split`. Property not found in
  4:   return x.split(' ');
              ^ Number

它清楚地说明了错误的位置和原因。只要我们将参数从数字更改为任何字符串,错误就会消失。

/*@flow*/

function foo(x) {
  return x.split(' ');
};

foo('Hello World!');

上面的代码就不会出现任何错误。

Nullable 类型

与其他类型系统相比,Flow以不同的方式处理null。它不会忽略null,因此它可以防止在传递null而不是其他一些有效类型时可能导致应用程序崩溃的错误。

思考以下代码:

/*@flow*/

function stringLength (str) {
  return str.length;
}

var length = stringLength(null);

上面代码中,Flow将抛出一个错误。要修复这个问题,我们必须分别处理null:

/*@flow*/

function stringLength (str) {
  if (str !== null) {
    return str.length;
  }

  return 0;
}

var length = stringLength(null);

我们加入了null检查,以确保代码在所有情况下都能正确工作。Flow将把最后这段代码视为有效代码。

Type 注解

类型检查是Flow的最佳特性之一,因为我们不必编写类型注释就可以获得有用的反馈。但是,在某些情况下,为了提供更好的检查并消除歧义,需要向代码中添加注解。

/*@flow*/

function foo(x, y){
  return x + y;
}

foo('Hello', 42);

Flow不会发现上面的代码中错误,因为 + 操作符可以用于字符串和数字,而且我们没有指定add()的参数必须是数字。

在这种情况下,我们可以使用类型注释来指定所需的行为。类型注释的前缀是:,可以放在函数参数、返回类型和变量声明上。

如果我们在上面的代码中添加类型注释,最终代码会变成:

/*@flow*/

function foo(x : number, y : number) : number {
  return x + y;
}

foo('Hello', 42);

这段代码显示了一个错误,因为当我们传入字符串时,而函数期望的是数字类型。

终端显示的错误如下:

index.js:7
  7: foo('Hello', 42);
         ^^^^^^^ string. This type is incompatible with the expected param type of
  3: function foo(x : number, y : number) : number{
                      ^^^^^^ number

如果我们传递一个数字而不是'Hello',就不会有任何错误。类型注释在大型和复杂的JavaScript文件中也很有用,可以指定所需的行为。

下面我们看看Flow支持的其他类型的注释。

Functions

/*@flow*/

/*--------- Type annotating a function --------*/
function add(x : number, y : number) : number {
  return x + y;
}

add(3, 4);

上面的代码显示了一个变量和一个函数的注释。add()函数的参数以及返回的值应该是数字。如果我们传递任何其他数据类型,Flow将抛出一个错误。

Arrays

/*-------- Type annotating an array ----------*/
var foo : Array<number> = [1,2,3];

数组注释采用Array<T>,其中T表示数组中各个元素的数据类型。在上面的代码中,foo是一个元素应该是数字的数组。

Classes

下面给出了一个类和对象的模式示例。惟一要记住的是,我们可以使用|符号在两种类型之间执行OR操作。变量bar1是根据Bar类的模式进行注释的。

/*-------- Type annotating a Class ---------*/
class Bar{
  x:string;           // x should be string       
  y:string | number;  // y can be either a string or a number
  constructor(x,y){
    this.x=x;
    this.y=y;
  }
}

var bar1 : Bar = new Bar("hello",4);

Object 字面量

我们可以用类似于类的方式注释对象文字,指定对象属性的类型。

/*--------- Type annonating an object ---------*/

var obj : {a : string, b : number, c: Array<string>, d : Bar} = {
  a : "hello",
  b : 42,
  c : ["hello", "world"],
  d : new Bar("hello",3)
}

Null

任何类型的T都可以通过写入?T而不是如下所示的T来包含null/undefined:

/*@flow*/

var foo : ?string = null;

foo 可以是字符串或者是null。

目前为止我们只是接触了Flow类型注释系统的皮毛而已。一旦您熟悉了使用这些基本类型,建议深入研究Flow文档

Library Definitions

我们经常遇到必须在代码中使用来自第三方库的方法的情况。在这种情况下,Flow会抛出一个错误,但是通常我们不想看到这些错误,因为它们会分散检查我们自己代码的注意力。

幸运的是,我们不需要修改库代码来防止这些错误。相反,我们可以创建一个库定义(libdef)。libdef只是JavaScript文件的一个术语,它包含第三方代码提供的函数或方法的声明。

让我们看一个例子来更好地理解上面具体在说什么:

/* @flow */

var users = [
  { name: 'John', designation: 'developer' },
  { name: 'Doe', designation: 'designer' }
];

function getDeveloper() {
  return _.findWhere(users, {designation: 'developer'});
}

上面的代码错误如下:

interfaces/app.js:9
  9:   return _.findWhere(users, {designation: 'developer'});
              ^ identifier `_`. Could not resolve name

产生这个错误是因为Flow不识别_。为了解决这个问题,我们需要为下划线引入libdef。

flow-typed

值得庆幸的是,GitHub上有一个名为flow-typed,包含了许多流行的第三方库的libdef文件。要使用它们,只需将相关定义下载到项目根目录下名为flow-typed的文件夹中。

为了进一步简化这个过程,可以使用命令行工具获取和安装libdef文件。通过npm安装:

npm install -g flow-typed

安装完成后,运行flow-typed安装将检查项目的package.json文件,并下载libdefs作为它找到的依赖项。

自定义libdefs

如果您使用的库在流类型存储库中没有可用的libdef,则可以创建自己的库。我不会在这里详细介绍,因为不是经常要做的事情,但是如果您感兴趣,可以查看文档

由于类型注释不是有效的JavaScript语法,我们需要在在浏览器中执行之前从代码中删除它们。如果您已经在使用Babel来转换代码,那么可以使用the flow-remove-types tool或作为Babel preset来完成此操作。下面只讨论第一种方法:

安装flow-remove-types依赖:

npm install --save-dev flow-remove-types

在package.json增加一个scripts:

"scripts": {
  "flow": "flow",
  "build": "flow-remove-types src/ -D dest/",
}

npm run build 将从src文件夹文件中删除所有类型注释,并将编译后的版本存储在dist文件夹中。编译后的文件可以像其他JavaScript文件一样加载到浏览器上。

另外,还有一些插件several module bundlers 可以删除注释。

结论

在本文中,我们讨论了流的各种类型检查特性,以及它们如何帮助我们捕捉错误并提高代码的质量。我们还看到了Flow是如何通过对每个文件进行“选择”,以及进行类型推断来非常容易地开始工作的,这样我们就可以开始获得有用的反馈,而无需在整个代码中添加注释。

你觉得JavaScript的静态类型检查怎么样?

您认为这是一种有用的工具,还是另一种给现代JavaScript带来更多复杂性的不必要的工具?