Flow 中的联结类型使用详解
luoshulin
8年前
<p><img src="https://simg.open-open.com/show/ab1de64483b66d0d4fb7572244ba2671.png"></p> <p>在Flow 的使用入门一文中,我们在语法层面简要介绍了 Flow 的实际使用;其中有一个联结类型,本文介绍联结类型的使用及相关知识点。</p> <h3>联结类型简介</h3> <p>「联结类型」是笔者的翻译,Flow 文档中的原名是叫「Union Type」;意即多个类型联结在一起。不过我也有看到译文称为「联合类型」的;译名暂时不重要,确保我们讨论的是同一个东西就行。</p> <p>联结类型的书写方式是多个类型用 | 符号联结在一起,如 type A = 1 | 2 | 3;不过如果类型名称较长,也可以分多行来写,每一个类型一行,注意:每个类型名前都要有 | 符号。</p> <pre> // @flow type T = | string | number</pre> <h3>联结类型的使用</h3> <p>联结类型可以将多个任意类型联结在一起,比如:</p> <pre> // @flow type A = 1 | 2 | 3; type B = number | 'a' | boolean; type C = 'a' | 1 | true | Array<string>; type D = A | B | C;</pre> <p>如代码所示,可以是多个字面量值类型的联结,多个基本类型的联结,甚至是多个联结类型的再次联结。从语义上联结类型可以理解成「或」;如类型为 A 的变量值可以是 1 或者 2 或者 3;是一个允许的类型的集合。</p> <p>既然提出了集合的概念,就有子集。在 Flow 的联合类型中;如果某个联结类型 T 满足某一约束要求,那么类型 T 的子集构成的联结类型,也满足这一约束要求;可以说,针对两个联结类型 A 和 B,如果 A 是 B 的子集,那么 A 是 B 的子类型。</p> <pre> // @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);</pre> <h3>联结类型的细化</h3> <p>函数参数接受一个联结类型是很常用的;比如 jQuery:既可以接受一个 CSS 选择符(string 类型),也可以接受一个函数(Function 类型);如果要给 jQuery 的参数写一个类型定义,大概是这样:</p> <pre> // @flow type T = string | Function; function jQuery(arg: T) { // ... } jQuery('div'); jQuery(function(){ // ... })</pre> <p>Flow 要求,如果一个函数接受一个联结类型,传入的参数类型可以是类型中的任意一个(one of those types);但是函数内部,必须对联结类型的每一种情况进行处理(all of the possible types);在 Flow 的官网中,把这个原则称为 requires one in, but all out ,而这个过程称为 <strong>类型的细化(Refinements)</strong> 。</p> <pre> // @flow function myMethod(value: string | number | boolean){ if (typeof value === 'string') { // Block A } else if (typeof value === 'number') { // Block B } else { // Block C } }</pre> <p>在上述代码中,定义 value 类型是 string、number 和 boolean 的联结类型;因此在函数内部对 value 进行处理时,必须进行区分;只有 value 是 string 类型值才会运行到 A 代码块;只有 value 是 number 类型值才会运行到 B 代码块;而在 C 代码块中,已经把联结类型中的 string 和 number 都排除了,所以这里可以安全得把 value 作为 boolean 类型处理。</p> <p>这样通过细化处理把联结类型拆解逐一处理,可以确保联结类型的安全使用。</p> <p>不过有一点神奇的是:在处理联结类型后,如果所有的可能类型都判断并处理了,并且还有其他的逻辑分支,Flow 是不做检查的。</p> <pre> // @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; } }</pre> <p>在代码块 C 中,对 value 进行的操作肯定是有冲突的;但是运行 Flow 却不会报错,因为 Flow 聪明的知道:在 A 和 B 代码块中,已经处理了 value 所有的可能性,程序运行是无法到达 C 代码块的。</p> <p>Flow 检查器必须知道每一个代码块的可到达性,以及运行到达这段代码块时变量的类型可能;这称为静态类型检查器的可到达性分析( reachability analysis )。</p> <h3>互斥类型的细化</h3> <p>在之前的例子中,我们是通过 JavaScript 语言中的 typeof 来判断变量到底是属于联结类型中的哪一个。但是在很多情况下,我们没法使用 typeof 来进行;比如多个对象类型的联结,那又如何处理呢?</p> <pre> // @flow type A = { name: string, } type B = { age: number, } type C = A | B;</pre> <p>类型 C 是 A 和 B 构成的联结类型;A 定义的类型,即要求该类型的变量,必须有 name 属性,取值是 string 类型(类似于 TypeScript 中的 Interface)。在 JavaScript 的代码中,区分 A、B 两种类型肯定能不能用 typeof,因为都是对象;针对这种类型,Flow 提供两种方案。</p> <p><strong>一、互斥类型</strong></p> <pre> // @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; } }</pre> <p>类型 Success 和 Failure 都有一个 <strong>同名的属性</strong> (success),且定义的类型是一个 <strong>精确的字面量类型</strong> ;Flow 将这样的多个对象类型称为互斥类型( disjoint unions )。他们之间就是根据这个同名的属性进行区分的。</p> <p>如果刚好你的业务数据中有可以用来区分的同名字段,可以使用互斥类型,否则还需要为了适应 Flow 的要求去修改业务数据结构。</p> <p><strong>二、确定结构的对象类型( exact object type ) </strong></p> <p>使用对象类型,我们只能限制对象中必须有哪些属性,这之外还有其他哪些属性是不限制的;如果我们能精确的定义,接受的对象类型参数只能有哪些属性,那就可以用 <strong>确定结构的对象类型</strong> 来做类型标注。 <strong>确定结构的对象类型</strong> 是在定义对象类型的时候,在大括号内部加上一对 | 符号,比如:</p> <pre> // @flow type T = {| name: string, age: number |}</pre> <p>这个类型的变量有且只有这两个属性。如果是两个这样的 <strong>确定结构的对象类型</strong> 构成的联结类型,在使用时根据各自独有的属性就可以进行区分了。</p> <p>理想的情况是这么用的:</p> <pre> // @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 检查是会出错的!!!</pre> <p><strong>不过遗憾的是,Flow 当前(v0.42.0)对此的实现是不太完善的。</strong></p> <p><strong>1.</strong> 从代码逻辑来讲,如果限制了参数 value 的类型必须是 C 的话,那么在 else 代码块里,可以确定 value 的类型只能是 B;但是 Flow 没有推断出来。如果你在使用中碰到这样的坑,目前可以这么修改你的代码使其通过 Flow 的检查。</p> <pre> // @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; } }</pre> <p><strong>2.</strong></p> <p>把上述代码中的 typeof 判断类型,改成用 in 操作符判断属性存在,符合 JavaScript 的语言逻辑,但是通不过 Flow 的检查。</p> <p><strong>3.</strong> 如果直接用 if(o.p) 来区分,能通过 Flow 的检查,但是从 JavaScript 语言逻辑来讲是不正确的;比如判断一个数字类型变量是否存在,数字为 0 时结果就不正确。</p> <p> </p> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/26401539</p> <p> </p>