Flow 中的联结类型使用详解

luoshulin 4年前
   <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>