Flow 中的联结类型使用详解

orangebird 发布于4月前 阅读1558次
0 条评论

Flow 中的联结类型使用详解

在Flow 的使用入门一文中,我们在语法层面简要介绍了 Flow 的实际使用;其中有一个联结类型,本文介绍联结类型的使用及相关知识点。

联结类型简介

「联结类型」是笔者的翻译,Flow 文档中的原名是叫「Union Type」;意即多个类型联结在一起。不过我也有看到译文称为「联合类型」的;译名暂时不重要,确保我们讨论的是同一个东西就行。

联结类型的书写方式是多个类型用 | 符号联结在一起,如 type A = 1 | 2 | 3;不过如果类型名称较长,也可以分多行来写,每一个类型一行,注意:每个类型名前都要有 | 符号。

// @flow
type T = 
  | string
  | number

联结类型的使用

联结类型可以将多个任意类型联结在一起,比如:

// @flow
type A = 1 | 2 | 3;
type B = number | 'a' | boolean;
type C = 'a' | 1 | true | Array<string>;
type D = A | B | C;

如代码所示,可以是多个字面量值类型的联结,多个基本类型的联结,甚至是多个联结类型的再次联结。从语义上联结类型可以理解成「或」;如类型为 A 的变量值可以是 1 或者 2 或者 3;是一个允许的类型的集合。

既然提出了集合的概念,就有子集。在 Flow 的联合类型中;如果某个联结类型 T 满足某一约束要求,那么类型 T 的子集构成的联结类型,也满足这一约束要求;可以说,针对两个联结类型 A 和 B,如果 A 是 B 的子集,那么 A 是 B 的子类型。

// @flow
type Base = 1 | 2 | 3 | 4 | 5;
type Sub = 1 | 2 | 3;
function myMethod(arg: Base) {
  // ... code here
}
let a: Sub = 1;
myMethod(a);

联结类型的细化

函数参数接受一个联结类型是很常用的;比如 jQuery:既可以接受一个 CSS 选择符(string 类型),也可以接受一个函数(Function 类型);如果要给 jQuery 的参数写一个类型定义,大概是这样:

// @flow
type T = string | Function;
function jQuery(arg: T) {
  // ...
}
jQuery('div');
jQuery(function(){
  // ...
})

Flow 要求,如果一个函数接受一个联结类型,传入的参数类型可以是类型中的任意一个(one of those types);但是函数内部,必须对联结类型的每一种情况进行处理(all of the possible types);在 Flow 的官网中,把这个原则称为 requires one in, but all out ,而这个过程称为 类型的细化(Refinements)

// @flow
function myMethod(value: string | number | boolean){
  if (typeof value === 'string') {
    // Block A
  } else if (typeof value === 'number') {
    // Block B
  } else {
    // Block C
  }
}

在上述代码中,定义 value 类型是 string、number 和 boolean 的联结类型;因此在函数内部对 value 进行处理时,必须进行区分;只有 value 是 string 类型值才会运行到 A 代码块;只有 value 是 number 类型值才会运行到 B 代码块;而在 C 代码块中,已经把联结类型中的 string 和 number 都排除了,所以这里可以安全得把 value 作为 boolean 类型处理。

这样通过细化处理把联结类型拆解逐一处理,可以确保联结类型的安全使用。

不过有一点神奇的是:在处理联结类型后,如果所有的可能类型都判断并处理了,并且还有其他的逻辑分支,Flow 是不做检查的。

// @flow
function myMethod(value: string | number ) {
  if(typeof value === 'string') {
    // Block A;
    var a: string = value;
  } else if(typeof value === 'number') {
    // Block B;
    var b: number = value;
  } else {
    // Block C;
    var c: Object = value;
    var d: Array<*> = value.push('a');
    var e: number = value - 1;
  }
}

在代码块 C 中,对 value 进行的操作肯定是有冲突的;但是运行 Flow 却不会报错,因为 Flow 聪明的知道:在 A 和 B 代码块中,已经处理了 value 所有的可能性,程序运行是无法到达 C 代码块的。

Flow 检查器必须知道每一个代码块的可到达性,以及运行到达这段代码块时变量的类型可能;这称为静态类型检查器的可到达性分析( reachability analysis )。

互斥类型的细化

在之前的例子中,我们是通过 JavaScript 语言中的 typeof 来判断变量到底是属于联结类型中的哪一个。但是在很多情况下,我们没法使用 typeof 来进行;比如多个对象类型的联结,那又如何处理呢?

// @flow
type A = {
  name: string,
}
type B = {
  age: number,
}
type C = A | B;

类型 C 是 A 和 B 构成的联结类型;A 定义的类型,即要求该类型的变量,必须有 name 属性,取值是 string 类型(类似于 TypeScript 中的 Interface)。在 JavaScript 的代码中,区分 A、B 两种类型肯定能不能用 typeof,因为都是对象;针对这种类型,Flow 提供两种方案。

一、互斥类型

// @flow
type Success = {
  success: true,
  value: boolean,
};
type Failure = {
  success: false,
  error: string
}
type Response = Success | Failure;

function handleResponse(res: Response) {
  if(res.success){
    var a: boolean = res.value;
  } else {
    var b: string = res.error;
  }
}

类型 Success 和 Failure 都有一个 同名的属性 (success),且定义的类型是一个 精确的字面量类型 ;Flow 将这样的多个对象类型称为互斥类型( disjoint unions

)。他们之间就是根据这个同名的属性进行区分的。

如果刚好你的业务数据中有可以用来区分的同名字段,可以使用互斥类型,否则还需要为了适应 Flow 的要求去修改业务数据结构。

二、确定结构的对象类型( exact object type

使用对象类型,我们只能限制对象中必须有哪些属性,这之外还有其他哪些属性是不限制的;如果我们能精确的定义,接受的对象类型参数只能有哪些属性,那就可以用 确定结构的对象类型 来做类型标注。 确定结构的对象类型 是在定义对象类型的时候,在大括号内部加上一对 | 符号,比如:

// @flow
type T = {|
  name: string,
  age: number
|}

这个类型的变量有且只有这两个属性。如果是两个这样的 确定结构的对象类型 构成的联结类型,在使用时根据各自独有的属性就可以进行区分了。

理想的情况是这么用的:

// @flow
type A = {|
  name: string
|}
type B = {|
  age: number
|}
type C = A | B;

function myMethod(value: C) {
  if (typeof value.name === 'string') {
    var a: string = value.name;
  } else {
    var b: number = value.age;
  }
}
// 这个例子 Flow 检查是会出错的!!!

不过遗憾的是,Flow 当前(v0.42.0)对此的实现是不太完善的。

1. 从代码逻辑来讲,如果限制了参数 value 的类型必须是 C 的话,那么在 else 代码块里,可以确定 value 的类型只能是 B;但是 Flow 没有推断出来。如果你在使用中碰到这样的坑,目前可以这么修改你的代码使其通过 Flow 的检查。

// @flow
type A = {|
  name: string
|}
type B = {|
  age: number
|}
type C = A | B;

function myMethod(value: C) {
  if (typeof value.name === 'string') {
    var a: string = value.name;
  } else if(typeof value.age === 'number') {
    var b: number = value.age;
  }
}

2.

把上述代码中的 typeof 判断类型,改成用 in 操作符判断属性存在,符合 JavaScript 的语言逻辑,但是通不过 Flow 的检查。

3. 如果直接用 if(o.p) 来区分,能通过 Flow 的检查,但是从 JavaScript 语言逻辑来讲是不正确的;比如判断一个数字类型变量是否存在,数字为 0 时结果就不正确。

关于上述几点的讨论,参考 how to refine different shape of Object types(exact object types)? 如果你开始在项目中使用 Flow,可能会碰到这个坑。

希望本文对你有帮助。

查看原文: Flow 中的联结类型使用详解

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。