Flow 的使用入门

Flow 的使用入门

Facebook 刚刚上线了重构之后的 Flow 官方站点,新的站点结构清晰、内容丰富。如果你想深入学习使用 Flow,最好的办法当然是直接看官方文档;不过如果你没有太多时间去看完整文档,我们为你准备了这篇 Flow 的基本入门知识。注意,本文不会分享如何具体安装以及在项目中配置 Flow,关于这些,你可以参考这篇文章

Flow 是什么?

可能有些知乎网友刚刚打开电梯,还不知道 Flow 是什么。我们从头说起:作为前端开发者,相信大家都经历过这些错误:

JavaScript 是一门动态类型语言,变量的类型是可以随时改变的;这种灵活性虽然可以使代码编写过程不用太多考虑类型的适配和转变,但是会提高在运行时产生错误的概率;比如刚才截图所示那些。

没有类型的静态检查是 JavaScript 语言的先天缺失,所有很多变量类型相关的问题只能在运行阶段暴露出来。为了使 JavaScript 语言的类型更加安全,业界的实践有 TypeScript;这些都需要你重新学习一套语言语法,然后由编译器把相应的代码编译成原生的 JavaScript 代码;在一个现有的系统中把当前代码改成 TypeScript 成本比较高,需要重写所有逻辑。

Facebook 推出的 Flow 是另一个思路。Flow 是一个静态类型检测工具;在现有项目中加上类型标注后,可以在代码阶段就检测出对变量的不恰当使用。Flow 弥补了 JavaScript 天生的类型系统缺陷。利用 Flow 进行类型检查,可以使你的项目代码更加健壮,确保项目的其他参与者也可以写出规范的代码;而 Flow 的使用更是方便渐进式的给项目加上严格的类型检测。那么这么好的 Flow,要怎么开始使用呢?

Flow 的使用初探

少说废话,先看东西。

// @flow
function getStrLength(str: string): number{ 
    return str.length; 
}
getStrLength(Hello World); 

上面这段代码定义了一个 getStrLength 函数,并且约束它接受一个字符串参数,返回一个数字;在运行 Flow 检查器时,任何对该函数的不合法使用都会报错,比如传入非字符串类型,在非数字上下文环境中使用该函数返回值;

你一定注意到了,这样改写 JavaScript 代码之后,这段代码是不符合 ECMA 语法规范的;所以在上线时,需要用插件把 Flow 的标注内容去掉;Flow 提供 flow-remove-typesbabel 插件 两种方式来去除标注内容,感兴趣的话可以点击相应链接,本文不详述。

如何对一个文件进行类型检查?

如果你想对一个文件进行类型检查,必须在文件头部加上一行注释;


// @flow 
// 或者下面这种
/* @flow */ 

意思简单明了,AT 一下 Flow 检查器,意思是:“嘿, Flow,来验一下我”。
当我们写完代码后,运行 flow check(或者配置的相应 npm 命令),Flow 会对加上了这行注释的 JS 文件进行类型检查,并且只会对加了这行注释的 JS 文件进行类型检查;这样可以方便的使你渐进式的把你项目中 JS 文件一个个的改写得符合规范。

自动的类型推导

在我们将一个 JS 文件加入类型检测的范围之后,运行 flow check, Flow 会怎么对这些文件进行操作呢?Flow 有一个自动的类型推导机制,很多情况下,Flow 都自动推导出变量的类型并持续跟踪其使用;比如:

let name = 'zhihu'; 

将变量 name 定义为 zhihu ,Flow 会自动推导 name 为 string 类型;如果后面的代码对 name 进行了不适用于字符串的操作,运行 Flow 命令对代码进行检测就会报错。比如:

// @flow
let name = ‘zhihu’ 
console.log(name - 1) // 即便没有手动给 name 加上类型标注,Flow 也会在这里报错

基本类型的类型标注语法

JavaScript 中的基本类型,类型标注语法是在变量后加上一个冒号,空格,然后是相应的类型名称,如:

// @flow

const a: string = 'zhihu'; 
const b: number = 5; 
const c: boolean = false; 
const d: void = undefined; 
const e: null = null;

以上需要注意的几点:
1. undefined 的类型是 void;
2. null 的类型是 null;
3. string 类型、number 类型和 boolean 类型,其类型名称都是小写开头;但是在 JS 中还有相对应的大写开头的类型名称,如 String,Number, Boolean;

在 Flow 中,大写开头的类型名和小写开头的类型名是有区别的。

// @flow

const a: string = 'a';             // 字面量值对应的类型名称是小写 
const b: string = String('b');     // String 函数是将参数转化成一个字符串,仍然是小写的 string 类型 
const c: String = new String(‘c’); // 大写开头的类型名称,其对应的值是 new 创建出来的类型实例;

字面量值作为一种类型

在 Flow 中,字面量值也可以作为一种类型,符合这种类型的变量只有这个字面量本身;给这个变量赋其他值, Flow 在进行类型检测时都会报错;比如下例所示。

// @flow
let monthsAYear: 12 = 12;
monthsAYear = 13; // Flow 会在这里报错

函数类型标注

对函数类型我们主要是标注其接受的参数类型和返回值类型;下面的示例中分别展示了如何对函数声明、函数表达式,以及箭头函数加上类型标注。

// @flow

// 函数声明 
function getLength(str: string): number {
 return str.length;
}
// 函数表达式
const greeting = function(welcome: string): void{
 console.log(welcome);
}
// 箭头函数
const addNumber = (a: number, b: number): number => (a + b);

数组类型标注


对数组的标注 Flow 提供两种语法,

1. Array 后接一对尖括号,且尖括号里边就是数组项的类型名称;

2. 类型名称后加一对中括号。

// @flow
const names: Array<string> = ['a', 'b', 'c'];
const ages: number[] = [1, 2, 3, 4];

元组(Tuple)类型的标注

另外一种常见的数组是元组(Tuple)。在其他语言里,元组可能是一个特定的类型结构;但是在 JS 里,元组就是数组来表示的,并且是一个有限数组,数组每一项的类型分别标注出来;通常的使用场景比如一个用来表示数据库纪录的数组,比如函数返回多个返回值。

// @flow
const recordItem : [number, string, boolean] = [1, 'First', true];

一个数组被标注为元组类型后,每一项的类型都不可再次改变,且数组的长度也不能改变;因此对元组类型的数组进行 push、pop 操作,都是 Flow 不允许的。

对象类型的标注


对对象类型加上类型标注的方式是定义其结构(Shape),即有哪些属性,属性及属性值的类型;

// @flow
const borderConfig : {
 width: number,
 color: string,
 hasShadow: boolean
} = {
 width: 10,
 color: 'red',
 hasShadow: true,
}

上面这种写法非常不直观,类型定义内容跟对象本身混在一起;优化的方式是像下面这样将类型定义和类型标注分开。

// @flow
type BorderConfigType = {
 width: number,
 color: string,
 hasShadow: boolean
}

const borderConfig : BorderConfigType = {
 width: 10,
 color: 'red',
 hasShadow: true,
}


type 是 Flow 中的关键字,用来定义自定义的类型,并且可以在后面的类型标注中使用。例如:

// @flow
type StringType = string;
const name: StringType = ‘zhihu’;

type TupleType = [ number, string ]
const record: TupleType = [ 1, ‘a’ ]

类的标注

Flow 支持对 ES6 中的类进行类型标注:包括类的属性和方法;类中用到的属性必须额外添加类型标注,并且是在与方法同一个层级(而不是在方法体内部)。

// @flow
class WrongClass1{
  method(){
    this.props = 1; // Flow 会报错,因为没有对 props 进行类型标注
  }
}

class WrongClass2{
  method(){
    this.props: number = 1; // Flow 还是会报错,对属性的类型标注必须与方法同一个层级
  }
}

class RightClass {
 props: number;            // 对,就像这样。
 method(){
   this.props = 1; 
 }
}

在定义好一个类型后,这个类本身就可以作为一个类型在对其他变量进行类型标注时使用。

// @flow
class MyClass{}
const mc: MyClass = new MyClass();

联结类型(Union Type)的使用

将两个或两个以上的类型,通过 | 符号进行联结,可以构成一个联结类型;假如有:

// @flow
type C = A | B;

类型为 C 的变量既可以是 A 类型值,又可以是 B 类型值;比如由于项目年久失修,其中有一个字段 user_id,有的地方取值是字符串,有的是数字类型,我们要给这种类型加上标注就适合用组合类型:

// @flow

type UserIdType = string | number;
let user_id : UserIdType = 12345678;
user_id = '87654321';

如果是定义函数的参数是一个联结类型,需要在函数的内部针对每种类型都作出判断并进行相应处理;这个过程称为类型的细化(Type Refinement)。

// @flow
type MsgType = string | number;
function show(msg: MsgType) {
 if (typeof msg === 'string' ){
   // do something
 } else {
   // 在这个代码块里,可以放心将参数 msg 当成数字类型
   // Flow 也会作出这样的推理
 }
}

交叉类型(Intersection Type)的使用

将两个或两个以上的类型,通过 & 符号并列写在一起,可以构成一个交叉类型;比如:

// @flow
type C = A & B;

定义为类型 C 的变量,其取值必须满足 A 类型约束,又满足 B 类型约束;

// @flow
type X1 = 1 | 2 | 3 | 4 | 5
type X2 =         3 | 4 | 5 | 6 | 7
type X3 = X1 & X2;

在以上代码中,X3 就是一个交叉类型。类型为 X3 的变量,其取值必须受 X1 类型约束,又受 X2 的类型约束,所以取值只可能是 3 或者 4 或者 5;

// @flow
type Y1 = {
  name: string,
  male: boolean
}
type Y2 = {
  name: string,
  age: number
}
type Y3 = Y1 & Y2;

而在这里例子中,类型那个 Y3 允许的取值是什么呢?按照 X3 的经验,很可能认为其取值必须是一个对象,且对象有一个 name 属性,因为毕竟 Y1 和 Y2 只有 name 这么一个共有属性。

这么理解是不对的;我们需要重新解读 Y1 和 Y2;符合 Y1 类型的值,必须是一个对象,且必须有一个 name 属性,属性值是字符串类型;且有一个 male 属性,属性值是布尔值类型;

Y2 同理;而 Y3 必须受到 Y1 和 Y2 的约束。因此 Y3 类型的值必须:

1. 是一个对象;

2. 有 name 属性,属性值是字符串;

3. 有 male 属性,属性值是布尔值;

4. 有 age 属性,属性值是数字类型。

// @flow
const wrong: Y3 = {  // Flow 会报错,因为缺少 male 和 age 属性
 name: 'zhihu'    
}

const right: Y3 = {  // 可以通过 Flow 的类型检测
  name: 'zhihu',
  male: true,
  age: 5
}
实际上,交叉类型(如 X3 或者 Y3)的取值是需要所有类型(构成该交叉类型的所有类型,如 X1 和 X2,Y1 和 Y2)的公共子类型值;关于类型与子类型的理论,是 Flow 中另一个难以理解的地方,甚至 Flow 的官方文档都写混淆了,我们会在下一篇跟你分享这个知识点。

对象的可选属性与变量的可选类型

这是两个容易弄混的概念;

// @flow
type Test = {
 key1: ?string,
 key2?: number,
}

在上面的例子中,我们定义了一个 Test 类型;注意它的两个属性后都有个问号,且问号的位置不一样:

一个问号是在冒号后,类型名称前;表示 Test 类型中,必须有 key1 属性,但是属性值不一定的 string,还可以是 null 或者 undefined;从另一个角度讲,只要 key1 值不是 null 和 undefined,就必须是 string 类型。这是 Flow 中 Maybe Type。在对这种类型的变量进行细化的过程中,也必须手工验证值是否为 null 和 undefined。

// @flow
function myFn(t: Test) {
  if (t.key1 !== null && t.key1 !== undefined ) {
    // 在这里,可以放心的将 t.key1 作为字符串类型进行操作
    console.log(t.key1.slice(0,t.key1.length))
  }
}

另一个问号是在冒号前,属性名后;表示 Test 类型中,可以没有 key2 属性,但是如果出现了 key2 属性,属性值必须是一个 number 类型。这是 Flow 中的 Optional object type properties。注意,如果允许没有 key2 属性,在 JavaScript 中,访问对象的某个不存在的属性,会返回 undefined;所以对象的可选属性,其属性值要么是指定的类型(如上例中的 number),要么就是 undefined,不能是 null。

any 类型

Flow 提供一个 any 类型;给变量标注为 any 类型后,如你预期的一样,这个变量可以是任意值;基本上相当于告诉 Flow:这个变量的类型我有把握,你不用管。在将 Flow 渐进的应用到你的项目中时,可能 Flow 针对某些变量的报错是不需要处理的,你可以标注为这种类型消除错误。

// @flow
let a: any = 1;
a = ‘a’;
a = { };

小结

当你开始使用 Flow 的时候,面临的需要进行类型标注的场景大部分都在上面了。另外还有一些有趣的高阶用法,比如泛型,$Keys<T>,$Diff<A, B>,比如 Flow 的类型理论,子类型与子集,我们会在后续的文章中持续分享;敬请关注。

本文内容参考:

编辑于 2017-04-10 13:54