为什么要使用 Flow ?

原文链接:https://blog.aria.ai/post/why-use-flow/

Flow是一个由 Facebook 开源的 JavaScript 静态类型检查器。旨在解决JavaScript编程中的多种痛点,写出更优雅、更易理解的 JavaScript 代码。

引用 Flow 主页上的介绍:

Flow 可以在 JavaScript 代码运行之前检测错误,包括

  • 静态类型转换,
  • 空指针引用,
  • 和可怕的调用函数未定义的错误

以及

Flow 还会逐步地将类型断言融入到你的代码中

所以 Flow 可以解决很多常见的 JavaScript 问题,你可以逐步将其引入你的代码库中。这很酷吧!

类型

在我们使用Flow之前,我们要先弄清楚什么是类型。我们看一下维基百科上的数据类型文章中的定义

类型是用来标识各种不同种类的数据的,如实数类型、整型或布尔型。它可以确定该类型的可能值,可对该类型的值进行的操作,其数据的含义,以及该类型的值可用的存储方式。

用我自己的话简单来讲,类型就是你程序中约束数据的规则,这些规则帮助计算机确定你可以在数据上做哪些事情,不能做哪些事情。如果你不小心试图破坏这些规则,它可以提醒你,这一点大有裨益。

当你使用不同的语言编写代码时,你会发现,类型的表现方式可能会迥然不同,从必须声明到可选,再到几乎不需要。通常,编程语言的类型系统分为两类:强类型 vs. 弱类型,以及动态类型 vs. 静态类型。

强类型 vs. 弱类型

维基百科有一篇很好的文章介绍这一点,目前人们普遍认为,强类型和弱类型有些模棱两可,因为二者之间还没有一个成文的约定。我决定按前面讲到的那篇维基百科上的文章来讲。

隐式类型转换和“类型双关”


在像 python 这样的强类型语言中,变量在第一次声明之后就不能再改变它的类型,除非你临时显式转换为其他类型,或者之后重新声明。

1
2
x = 5
print x + "" # cannot add integers to strings

这样会抛出下列错误

1
`TypeError: unsupported operand type(s) for +: 'int' and 'str'`

但这样写是可以的

1
2
3
x = 5
x = ""
print x + "" # redeclared x so it's fine

或者这样

1
2
x = 5
print str(x) + "" # casted x to a string so it's fine

在 JavaScript 等弱类型语言中,由于变量在使用时被隐式转换,会变得更加灵活。你可以将字符串和对象相加,数组和对象相加,数值和null相加。更糟糕的是,运算出错时并不会抛出异常。

1
2
3
4
5
console.log({} + {}) // NaN
console.log({} + []) // 0
console.log([] + []) // ''
console.log({} + 2) // [object Object]2
console.log({} + 'hello') // [object Object]hello

我想你能想象到可能发生的各种问题,而这些问题都不会抛出错误。

动态类型 vs. 静态类型

动态类型 vs. 静态类型比强类型 vs. 弱类型有更多的争议。我不会说二者哪个更好,也不会逐一全面分析它们的优点。相反,我只是简单介绍一下二者的优点,如果你想了解更多关于它们二者哪个更好的辩论,可以看看下面的文章。

现在给出我自己的免责观点:

在静态类型的语言中,你需要显式地写出变量的类型。很多人都了解 Java 这种强类型的静态类型语言,你需要在 Java 语言中写明变量类型,如intString,以及函数的返回值类型和参数类型,如int add(int a, int b)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Hello {
public static void main(String[] args) {
int x = 5;
int y = 10;
String s = "1.23131";

System.out.println(add(x, y)); // 15
System.out.println(add(x, s)); // Incompatible types: String cannot be converted to int
}

public static int add(int a, int b) {
return a + b;
}
}

因为String类型和int类型不能相加,这段代码在编译时第8行会报错。

注意:

  • 错误在编译的时候就会捕获,而不是这代码运行的时候,这意味着,只有你修复了错误才能运行代码。
  • 如果你使用IDE编写代码,IDE会提示你add(x, s)写法错误。因为你事先指定了语言类型,所以IDE可以在更高级别进行分析,而无须编译,以发现错误。
  • 如果函数被命名为其他很随意的名字而不是add,你仍然可以看出它需要接受两个整型参数并返回一个整型结果,这是非常有用的信息。

在动态类型语言中,你根本不用指明变量的类型。其主要好处是你的代码不会那么混乱,你不必在开始编程之前考虑类型,这提升了生产力。python 就是一门强类型的动态类型语言,下面的代码可以实现与上一段代码相同的功能。

1
2
3
4
5
6
7
8
9
10
def main():
x = 5
y = 10
s = "1.23131"

print add(x, y) # 15
print add(x, s) # TypeError: unsupported operand type(s) for +: 'int' and 'str'

def add(a, b):
return a + b

运行代码时,这段代码将在第7行抛出错误,因为string类型和int类型是不能相加的。

值得注意的几点:

  • 这样写代码更加简洁。
  • 你不能确定变量ab的类型,intstringfloat等类型都是有可能的。
  • 这段代码仍然会在运行时抛出错误,注意是在运行时而不是在编译时,这与静态类型语言比较是一个很大的区别。也意味着测试对于动态类型的语言更加重要,因为即使代码包含类型错误,也可能不会有太大问题。

静态类型语言中的类型推断

我之前说过,静态类型语言需要显式地写出类型,这句话并不完全正确。在没有类型推断的语言(如 Java )中,这是对的,但在类型推断语言中,计算机可以帮助你来确定使用什么类型。例如,在下面的例子中,是用 Haskell 写的一段与前文代码功能相同的一段代码,这是一门以其真正强大的类型系统而闻名的语言。在我写 let x = 1 let add’ = (+)`时,Haskell 会自动推断其类型,而不需要显式写出。

Haskell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main :: IO()
main = do
let x = 1
let y = 2
let s = ""

-- Type inference
let add' = (+)

print (add x y) -- 3
print (add' x y) -- 3

print (add x s) -- throws error

-- With Explicit Types
add :: Int -> Int -> Int
add = (+)

在 Flow 等其他类型系统中都有类型推断。然而,即使类型推断让你的编码更加简单,编码量更加少,你也不应该完全依赖于它。

回到JavaScript和Flow上面来

现在我们了解了更多关于类型的东西,可以回到手头上的问题来,这会让你在写 JavaScript 的时候犯更少的错误。

JavaScript 既是弱类型语言又是动态类型语言,这是一个灵活但又及其容易出错的组合。正如前文所说,由于隐式转换的原因,不同类型的值之间所有的操作都不会抛出错误,无论这些操作是否有效(弱类型),并且在从来不用显式地写出类型(动态类型)。

这种动态类型和弱类型的大杂烩是相当不幸的,从下面的例子和许多人对这门语言的批判中就可以体现。

这些问题的大多数解决方案是 Flow,通过静态类型和类型推理,如前文所述,解决了许多语言的痛点。

这不是一篇教程,如果你需要教程可以移步flow入门指南

让我们继续分析,回到我们在讲弱类型部分的第一个JS示例,这次我们使用 Flow 来检查代码。

我们将// @flow添加到代码的第一行,然后使用命令行工具flow来检查代码(集成在IDE中也是有可能的):

1
2
3
4
5
6
7
8
9
// @flow
// ^^^^^ that's necessary to activate flow
// flow is opt-in to allow you to gradually add types

console.log({} + {}) // NaN
console.log({} + []) // 0
console.log([] + []) // ''
console.log({} + 2) // [object Object]2
console.log({} + 'hello') // [object Object]hello

于是每一行代码都会立即生成类似于下面的错误。

1
2
3
4
5
index.js:3
3: console.log({} + {}) // NaN
^^ object literal. This type cannot be added to
3: console.log({} + {}) // NaN
^^^^^^^ string

不需要做任何额外的工作来添加类型注释,Flow 就已经指明了代码中的错误。wat视频中提到的那些问题也不会再出现。

注释代码的好处

虽然 Flow 会帮助发现上面的错误,但是你要想真正从中获益,你必须自己编写类型注释,这意味着你可以使用 Flow 的内置类型,如numberstringnullboolean等等,来指定值的类型或自定义某些类型别名,请看下面的例子:

1
2
3
4
5
type Person = {
age: number,
name: string,
gender: 'male' | 'female'
}

现在你可以将函数:

1
2
3
function xyz(x, y, z) {
return x + y + z
}

转换成

1
2
3
4
5
// @flow

function xyz(x: number, y: number, z: number): number {
return x + y + z
}

在上述实例中,我们知道函数的参数x、y、z应该接受三个数值,并且其返回值也应该是数值。如果你试图这样传参,xyz({}, '2', []),在JavaScript是百分之百可以的,而 Flow 则会抛出错误。随着你越来越多地开始这样写,Flow 会更加了解你的代码库,并更好地提示你代码中的错误。

一些例子

捕获函数参数数量不正确的错误。

代码如下:

1
2
3
4
5
6
7
// @flow

function xyz(x: number, y: number, z: number): number {
return x + y + z
}

xyz(1, 2)

报错如下:

1
2
3
4
5
6
7
index.js:7
7: xyz(1, 2)
^^^^^^^^^ function call
7: xyz(1, 2)
^^^^^^^^^ undefined (too few arguments, expected default/rest parameters). This type is incompatible with
3: function xyz(x: number, y: number, z: number): number {

^^^^^^ number

捕获函数参数类型不正确的错误。

代码如下:

1
2
3
4
5
6
7
// @flow

function xyz(x: number, y: number, z: number): number {
return x + y + z
}

xyz(1, 2, '')

报错如下:

1
2
3
4
5
6
7
index.js:7
7: xyz(1, 2, '')
^^^^^^^^^^^^^ function call
7: xyz(1, 2, '')
^^ string. This type is incompatible with
3: function xyz(x: number, y: number, z: number): number {

^^^^^^ number

确认你不要忘记检查 NULL。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
// @flow

function xyz(x: number, y: number, z: number): ?number {
return Math.random() < 0.5 ? x + y + z : null
}

function printNumber(x: number): void {
console.log(x)
}

printNumber(xyz(1, 2, 3))

报错如下:

1
2
3
4
5
6
7
index.js:11
11: printNumber(xyz(1, 2, 3))
^^^^^^^^^^^^^^^^^^^^^^^^^ function call
11: printNumber(xyz(1, 2, 3))
^^^^^^^^^^^^ null. This type is incompatible with
7: function printNumber(x: number): void {

^^^^^^ number

确认你的返回值是否正确。

代码如下:

1
2
3
4
5
6
7
// @flow

function xyz(x: number, y: number, z: number): number {
return Math.random() < 0.5
? x + y + z
: null
}

报错如下:

1
2
3
4
5
index.js:6
6: : null
^^^^ null. This type is incompatible with the expected return type of
3: function xyz(x: number, y: number, z: number): number {
^^^^^^ number

确认对象包含所有它应该包含的属性。

代码如下:

1
2
3
4
5
6
7
8
9
// @flow

type Person = {
age: number,
name: string,
gender: 'male' | 'female'
}

const person: Person = { name: 'joe', age: 10 }

报错如下:

1
2
3
4
5
index.js:9
9: const person: Person = { name: 'joe', age: 10 }
^^^^^^ property `gender`. Property not found in
9: const person: Person = { name: 'joe', age: 10 }
^^^^^^^^^^^^^^^^^^^^^^^^ object literal

确认不存在的对象属性不被访问。

代码如下:

1
2
3
4
5
6
7
8
9
10
// @flow

type Person = {
age: number,
name: string,
gender: 'male' | 'female'
}

const person: Person = { name: 'joe', age: 10, gender: 'male' }
console.log(person.job)

报错如下:

1
2
3
4
5
index.js:9
9: console.log(person.job)
^^^ property `job`. Property not found in
9: console.log(person.job)
^^^^^^ object type

更深入的探讨

还有一些常见的好处,我可能忘了。但上面的例子已经涵盖了绝大部分。如果你在想,“就只有这些吗?”,那么还可以进行更加深入的研究。

Giulio Canti 写了相当多的关于 Flow 更高级的东西,而不仅仅是使用 Flow 来限制变量类型、参数类型以及返回类型。

他还撰写了flow-static-land,这是很让人兴奋的。

长话短说的总结

  • JavaScript 既是弱类型语言又是动态类型语言,极其容易出错,也是它成为糟糕语言的一个重要原因。
  • 由于前期成本很低,并且具有缓慢演进的能力,Flow 通过向 JavaScript 添加类型系统来解决这两个问题。