flow是FB推出的代码静态检查工具,目的是让javascript开发者能“更快”,“更好”,“更有信心”地编写代码。在它出现之前,想要做JS的静态检查只能使用typescript,但是typescript相对javascript而言由于加入了很多新的语言特性,学习成本较高,而且对于旧项目基本上需要重写代码,在一定程度上增加了使用难度。当然两者各有优劣,使用两者之中的哪一个更多取决于团队的配置和成本。在现阶段javascript应用功能愈发复杂,工程量越来越庞大的背景下,加入静态检查也变得愈发重要。
什么是静态检查
总所周知,javascript是一门弱类型语言,也就是常说的动态语言,而java
,c#
,c++
这些语言属于静态语言,即强类型语言。
如果用js写一个求和函数
1 | function add(x, y){ |
如果使用c#
1 | int add(int x, int y){ |
接下来在某处使用 add('x', 5)
如果是js的话,编写时一切正常,因为在代码执行前,并不会抛出任何错误。
但是如果是c#之类的强类型语言,在编程时,IDE就会抛出错误,告诉我们'x'
参数不合法,编译也不会通过。
这就是静态检查的优势,它能在程序运行之前就抛出一些可能的错误,开发人员在编程时就能规避这些问题,而不是在运行时。
现在使用flow给刚刚的方法加入静态检查
1 | /** |
引入flow后,每当使用flow
命令,或者在IDE上集成flow插件后,控制台都会抛出错误。这些错误在编写时就会抛出,而不需要在浏览器上运行,甚至有的隐式错误,加入静态检查后也能及时排查,这对开发效率以及提升代码健壮性都有很大的帮助。
需要注意的是,flow是可开关的,只有在文件顶部注释上引入// @flow
后,这个文件才会开启检查。也就是说,对于旧的项目可以渐进式地加入flow,而不需要整体重构。
初始化Flow
安装
首先通过npm安装
1 | npm i flow-bin |
然后添加脚本到package.json
1 | "flow": "npm run flow" |
添加编辑器支持(For vscode)
在扩展程序中搜索Flow Language Support
,安装重启后,问题面板
会出现flow的错误提示。
更多详情请参考:安装
类型声明
使用类型声明(Type Annotations)是使用flow的第一步。如上文的例子,加入类型声明后,flow会知道你的参数,变量,函数等属于哪种类型,如果传入了不匹配的类型,就会抛出错误。
基本类型(Primitive Types)
flow对javascript的几种基本数据类型预设有对应的声明类型。(如果你对基本数据类型不是很清楚,点击这里)
数据类型 -> 声明
- String -> string
- Boolean -> boolean
- Number -> number
- null -> null
- undefined -> void
示例:
1 | // @flow |
如果数据被包裹成了对象,那么类型名称要使用大写。
示例:
1 | // @flow |
flow并不会自动转化数据类型,即使对string
类型传入new String('1')
,仍然不会通过检查。
注意
new String('a')
和String('a')
返回的不是一种类型,前者返回String
对象实例,后者返回基础数据类型string
。
可能类型(Maybe Types)
在js中很多函数参数是可选的,使用可能类型Maybe Types
可以很方便地对此种参数进行声明。被声明成此类型的属性,可以为undefined
或者null
。
1 |
|
当一个参数是可能类型时,传入
null
,undefined
均是合法的。如果一个参数有默认属性
(name: string = 'hello')
,这个参数也是可选的,但是不能传入null
。除了函数参数是可选的,对象属性也可以声明成可能类型的,但是对象参数也必须声明,不能不传。
1 | //@flow |
注意可能类型
和对象的可选参数
不是一个概念,一个是针对声明类型,一个是针对对象的属性。对象的可选参数只能传入undefined
或声明的类型,不能传入null
,相当于Type | void
1 | //@flow |
文字类型(Literal Types)
文字类型比较简单,声明后只能接受单个特定的数据。
1 | // @flow |
混合类型(Mixed Types)
当变量可能有多种类型的时,可以使用mixed
进行声明。但是使用mixed
声明的变量时必须对其类型进行判断,不然flow会抛出错误。
mixed
也是一种安全的声明,使用它并不是说就可以为所欲为,这点和any
(下面会说)有本质上的区别。
1 | //@flow |
任意类型(Any Types)
一旦对变量声明为any
,意味着flow不会对这种类型的数据进行检查。如果你的声明全部是any
,那就和普通的javascript文件没有区别了。因此在开发时应该尽量避免使用any
进行声明。
此外,any
声明的变量后续处理的数据都会变成any
类型,造成的后果是整个调用链都会失去静态检查。
1 | //@flow |
为了避免这种情况,应该尽量对已知变量标记类型:let c:number = obj.num;
当然,在某些场景下还是可以使用any
的
- 确实无法判断数据的类型,例如未加入flow的三方库。
- 因flow本身造成的无法排除的错误。
变量类型(Variable Types)
当使用let
,const
,var
初始化变量时,flow会隐式的自动生成声明。例如 let a = 1
,flow会自动把a
标记成number
类型。
此外flow有很强大的推导能力,可以自动分析代码并找出可能的错误。
1 | // @flow |
函数类型(Function Types)
对函数进行声明
1 | // @flow |
得益于flow的自动推导能力,对于有些函数并不需要指定参数的类型。
1 | // @flow |
声明函数类型
1 | // @flow |
函数的this
flow不需要指定函数的this,它自动分析运行函数时的上下文。
1 | // @flow |
断言函数
有时候flow可能会让人感觉困惑,比如这种情况。
示例:
1 | // @flow |
如果把判断逻辑封装成函数,就会抛出异常
1 | // @flow |
这确实让人迷惑,但是也是情理之中,因为声明新函数后,函数的内部逻辑是未知的,也许开发者并没有判断a
和b
的值就直接返回true
了呢?就会导致concat
方法返回一个意料之外的值。
为了应对这种情况,flow提供一个%checked
语法将一个函数声明成断言函数。当一个函数声明成断言函数后,函数内部不能声明任何变量,此外flow会对函数体逻辑进行推导,保证返回值和业务逻辑相匹配。
经过改造后,truthy
函数就会变成这样
1 | // @flow |
因为断言函数始终返回boolean
类型,所以返回值声明: boolean %checks
可以简写成: %checks
。
Function类型
一旦声明成Function
类型,这个函数的返回值和参数可以为任意值,应该尽量避免使用。
Object类型
object在javascript中有很多种不同的使用方式,对于这些使用方式flow也提供了多种声明的方法。
对象声明语法
声明一个对象类型和声明一个对象类似。
1 | // @flow |
如果尝试访问或赋值未声明的属性,flow会认为该属性是不存在的并抛出错误。
1 | // 接上面的例子... |
对象属性可以声明成可选的。(注意与可能类型(Maybe Type)的区别)
1 | // @flow |
对象类型推导
如果没有显式地声明类型,flow会自动推导对象的类型,推导的结果取决于使用对象的方式。
封闭对象
如果创建一个拥有多个属性的对象(这里称为封闭对象),flow会推导出这个对象的类型,与显式地声明对象类型效果一致。
1 | // @flow |
开放对象
如果创建一个空的对象({}),就创建了一个开放对象。开放对象的属性类型是未知的,所以允许添加新的属性。但是这并不是说flow不会检查开放对象的属性,相反地,flow会对属性的类型像变量类型一样进行推导。
1 | // @flow |
由于加入了条件语句,flow在进行推导后得出obj.bar
可能为boolean
或者number
,所以对d:number
赋值时抛出了错误,真的是超级智能有没有?
需要注意的是,对于开放的对象来说,由于在编写代码时可能在任何地方对该对象进行属性赋值,所以flow并不确定某个属性是否存在,因此不会对不确定的属性进行类型检查。如下
1 | var obj = {}; |
严格匹配类型
在flow中,如果传入一个有额外属性的对象被认为是安全的。比如
1 | // @flow |
如果需要强制限定只能传入声明的属性,可以在给声明的对象加入|
边界,将其声明成严格匹配类型。
1 | // @flow |
需要注意的是,如果一个对象声明成为了严格类型,混合对象类型只能使用对象延展,而不能使用交叉类型(Intersections Type)。
交叉类型(Intersections Type)在下文会有详解。
1 | // @flow |
对象映射
flow提供了一种特殊的属性声明:索引属性,可以让对象添加任意符合键值类型的属性。
1 | // @flow |
需要注意的是,使用索引属性时flow并不能知道属性的是否存在,因此不会对属性进行检查,这点和开放对象是类似的。如上面的obj['john'].toFixed(0);
只有在运行时才会抛出错误。
Object
类型
一旦变量声明成Object
,传入任意对象都是合法的。只有在参数是对象但又无法确定对象的属性时才能使用,否则应该尽量避免使用它。
数组类型
声明数组
有2种方式声明数组类型,Array<T>
和T[]
。
1 | // @flow |
不安全访问
访问一个数组类型的数组是不安全的,flow无法对齐进行检查。
1 | // @flow |
为了让类型是安全的,在取值时应该对变量额外加入void
声明。
1 | // @flow |
只读数组
如果需要将一个数组声明成只读,只需要将Array<T>
换成$ReadOnlyArray<T>
即可。
只读数组无法增加或者删除元素,但是元素自身的属性是可修改的。
1 | // @flow |
使用只读数组还有个好处就是,$ReadOnlyArray<number|string>
是$ReadOnlyArray<number>
的子类型,但是Array<number|string>
并不是Array<number>
的子类型。
1 | // @flow |
这个示例中,array
是Array<number>
类型,作为参数传入someOperation
会报错误。因为someOperation
希望接受的参数是Array<number | string>
,而在函数内部可能会执行pop('123')
这样的插入操作,对arr
参数的声明而言这是合法的,但对于array
却是非法的,因为它只能接受number
类型的数据。
在这种情况下,arr
只能声明成$ReadOnlyArray<number|string>
只读类型,这样可以保证arr
参数不会改变,flow就可以保证array
对象是安全的,也就不会报错了。
1 | // @flow |
元祖类型(Tuple Types)
元祖数据是由数组创建的长度和内容确定的一组数据,类似于数据库表的一条记录。
flow使用[type, type, type]
声明这一类数据,与数组不同的是,内部元素的状态是可检查的。如果访问越界的索引,也会抛出错误。
1 | // @flow |
此外,元祖类型还有以下特点
- 只能匹配长度相同的类型
- 与任何数组类型都不匹配
- 不能使用改变
push
,pop
等方法改变数据
类类型(Class Types)
在Flow中,声明的类(Class)不仅是值,还可以当做类型来使用。
1 | // @flow |
类方法
声明一个类方法和声明函数类型一致。
1 | // @flow |
类属性
1 | // @flow |
如果在类方法中访问一个未声明的属性,flow会抛出错误,需要添加属性的声明。
1 | // @flow |
如果使用类属性语法设置了值,flow会对属性类型进行推导,不需要显式的声明类型。
1 | // @flow |
类泛型
类同样支持泛型
1 | // @flow |
标称和结构
需要注意的是,flow支持类的是标称类型
,而不是结构类型
,意味着相同结构的类并不是匹配的。
1 | // @flow |
虽然MyClassA
和MyClassB
结构一致,但是类型并不匹配。这里是官方说明。
类型别名(Type Aliases)
上面都是在变量,函数等上面声明类型,但是如果有的类型想要进行复用要怎么办呢?flow提供了type
关键字,帮助声明可以复用的类型别名。
1 | // @flow |
语法
类型别名用type
进行声明,并且可以用=
相互赋值
1 | // @flow |
类型别名也支持泛型
1 | // @flow |
接口类型(Interface Types)
上面提到flow支持类的是标称类型
,而不是结构类型
,意味着相同结构的类并不是匹配的。如果想要让具有相同结构的类能共用一种声明结构呢?flow提供了interface
关键字来声明一种数据结构。
1 | // @flow |
也可以使用implements
关键字对类的实现做约束
1 | // @flow |
不难看出,interface
和很多静态语言的语法一样,声明了类的属性和方法,拥有相同interface
实现的类之间是类型安全的。
类还可以实现多个接口
1 | class Foo implements Bar, Baz { |
Interface语法
interface
的声明和对象的声明基本一致。
不可变数据
interface
类型的属性是默认不变(invariant)的,但是可以通过额外的修饰符将其声明成协变(covariant)或逆变(contravariant)。这几个名词可能让人有点迷惑,在这里我们简单理解成只读和只写就好了。
invariant的概念后面会单独用文章来说明。有兴趣的同学可以先看看官方文档。
- covariant类型允许传入更加确定的属性,且数据是只读的
- contravariant类型允许传入更少确定的属性,且数据是只写的
1 | // @flow |
泛型(Generic Types)
泛型又称“多态类型”,是将类型抽象的一种方式。上面介绍其他类型的时候,有些地方已经提到和使用了泛型。
1 | // @flow |
这个例子中,定义了一个方法,返回值的类型传入参数的类型一致。
flow不会推导泛型,因此如果想要使用泛型,需要显式的声明,不然flow的推导结果可能会比你想象的要缺少多态性。
1 | // @flow |
在这个示例中,flow会将identity
函数的返回值推导成string | number
,然后a
和b
的验证会抛出验证错误。将identity
显式的声明成泛型,一切正常。
1 | // @flow |
泛型的行为
泛型的参数(T)就像是“变量”一样,可以在作用域任意地方的作为类型使用。
可以在泛型的参数列表中定义多个参数。
1 | // @flow |
- flow会跟踪传入的值,保证泛型不会被设置成某个确定的类型。
1 | // @flow |
- 在flow中,传入一个类型后经常会丢失原有的类型,例如将
'john'
作为参数传入identity(value: string)
后,value
会丢失原始的文字类型'john'
。使用泛型则可以保证值的类型不会变化。
1 | // @flow |
- 泛型的类型是未知的,但可以给泛型的参数声明类型来进行约束。
1 | // @flow |
经验证,如果给泛型的参数声明了类型会导致非基本类型数据失去原有的文字类型。所以上面例子中
var log: {foo: '1'} = logFoo({foo: '1'})
会抛出错误。
- 可以给泛型像函数一样传入参数来指定泛型参数的类型,还可以传入默认的类型。此特性仅对
Classes
,Type
,Interface
类型有用。
1 | // @flow |
联合类型(Union Types)
当希望一个类型拥有多个可选值时,联合类型就派上用场了。
定义一个联合类型格式如下
1 | Type1 | Type2 | ... | TypeN |
联合类型并不是指“并集”,而是取多个值的其中一个。
传入多类值,返回单类值
下面的例子中,函数接受类型为number | boolean | string
的参数,并返回一个string
类型的值,由于函数体中没有判断所有可能值,也没有返回一个默认值,所以函数可能返回undefined
,这是开发过程中很常见,也是容易疏忽的隐式错误。flow通过检查发现可能返回的值与返回类型string
不匹配,所以抛出了检查错误,帮我们规避这方面的问题。
1 | // @flow |
拆解联合
假设接口返回了这样的数据结构。
1 | // @flow |
如果success
为true
,value
有值
如果success
为false
,error
有值
1 | // @flow |
因为value
和error
是可能类型,所以验证都失败了,但是我们设想的却不应该是这样。于是我们转变思路,由于将Response
拆解联合。
拆解联合的必要条件是每种类型可以通过一个公共的属性加以区分。显然Response
满足这个条件。
1 | // @flow |
当response.success
为true
时,flow会自动将response
选择成Success
,然后一切正常。
严格拆解联合
那如果没有一个公共属性可以加以区分呢?看看另一个情况,如果接口返回的是这样的结构
1 | // @flow |
按上面的方式拆解
1 | // @flow |
由于传入额外的属性{error: true, message: string, success:true}
对于Failed
规则也是匹配的,在函数体中的判断通过后,response.error
会是undefined
,flow认为这种情况是会有隐式错误的,所以抛出了错误。
别忘了之前提到过严格匹配,这里使用它可以解决上面的问题。
1 | // @flow |
交叉类型(Intersections Type)
交叉类型会将多个类型合并成为一个类型,类似于集合的“并集”操作。语法上使用&
将多个类型连接。
1 | Type1 & Type2 & Type3 |
比如
1 | // @flow |
如果合并的类型有相同的属性,这个属性的类型同样会进行交叉操作,但是这里有一点有点和预期不同。
1 | // @flow |
1 | // @flow |
flow文档上并没有对这种情况作说明,文档上说会进行交叉,但实际情况相同属性必须B
中的属性是A
的子类型才能正常交叉,针对这个问题已经提交issue。
typeof类型
typeof
在javascript中用于获取一个值的类型,在flow中typeof
也是用于获取类型,不过获取的是flow的声明类型。
1 | // @flow |
值得注意的是,如果对文字没有显式声明类型,flow会将其推导成基本类型,所以typeof
得出的也是基本类型。但是如果对文字显式声明了文字类型,typeof
就会得到显式声明的类型。
1 | // @flow |
类型转换(Type Cast)
语法
(value: Type)
这种格式并不会转变原数据类型,返回值才会是转换后的类型
类型断言
类型转换的一个重要作用就是断言类型是否是想要的类型。
1 | // @flow |
转换
如果对一个变量进行了类型转换,它就会变成转换后的类型。下面的例子中,value
并未显式指定类型,因此它在上下文中的类型既能为42
,也能是number
。当后续将它的类型转换成number
后,它的类型变不再可以为42
了。
1 | // @flow |
通过any转换类型
1 | let value = 42; |
我们尝试将value
的类型转换成string
,显然会抛出错误。但是如果先将其转换成any
,那么它就可以转换成其他任何类型了。
这种做法显然是不安全的,也不推荐这样做。但是在有些情况下,如果能确定结果的类型,但是flow难以识别类型,可以使用any做一次中转。
1 | // @flow |
上面例子中,flow
只能将clone
识别成空对象,如果直接返回,后面的断言操作全会抛出异常。但是使用any
将数据类型做一次转换后,可以将其转换成和obj
参数一样的类型。因为从代码层面可以确定返回值的类型和传入的值是一致的。
模块类型(Module Types)
类似于esmodule
,flow的类型可以作为模块导入导出。
1 | // @flow |
1 | // @flow |
此外,flow支持使用typeof
导入模块的类型。
1 | // @flow |
1 | // @flow |
以上介绍了大部分flow的类型,由于工具类型(Utility Types)
内容比较多,所以后面会单独来介绍。
结语
参考文档: