最近在看React.js,可能大家知道在React中,很大一部分源码都是用Flow.js写的,有时候会看不懂是什么意思。所以阅读源码之前,还是有必要快速看一下Flow.js的语法的。
那Flow.js是什么框架呢?其实它和最近几年非常流行的TypeScript类似,是一个静态类型检查器,我们知道JavaScript是一个弱类型语言,在大型项目、框架实行的过程中,这种弱类型特征会带来一些意想不到的Bug等风险,所以需要引入类型检查器,从根本上避免类型带来的Bug。
Vue3.js就引入了TypeScript,React.js在构建的时候,TypeScript并不成熟,Facebook内部首先构建了自己的类型检查系统Flow.js,我们要知道Flow实际上是一个内部开源项目,它首先是为React和其他Facebook的JS框架服务的,因此学习Flow.js在我们实际编写上收获并不会比TS大,作者编写本文的目的也是仅仅为了希望阅读React源码的读者能够看懂源码的程度,用一个词来说就是浅尝辄止。
比较特殊的,需要注意的东西会包含在本文,特别基础、不需要知道也能看出来什么意思的不在本文的内容范围。
本节给出Flow.js声明类型的快速示例。
// @flow
function concat(a: string, b: string) {
return a + b;
}
concat("A", "B"); // 成功
concat(1, 2); // 错误
我们在参数上设置了类型边界(string),在接收到错误类型时Flow会报错。
本节将简明给出所有类型的注释方法。
原始类型包括:
null
undefined
(void
in Flow types)原始类型有两种:
字面值
true;
"hello";
3.14;
null;
undefined;
包装器对象
new Boolean(false);
new String("world");
new Number(42);
在Flow中声明原始值分为小写(字面值)和首字母大写(包装器对象)。
声明为boolean
类型之后,只接受true
, false
以及表达式判断。
// @flow
function acceptsBoolean(value: boolean) {
// ...
}
acceptsBoolean(true); // 成功
acceptsBoolean(false); // 成功
acceptsBoolean(0); // 错误
acceptsBoolean("foo"); // 错误
acceptsBoolean(Boolean(0)); // 成功
acceptsBoolean(!!0); // 成功
需要注意的是,NaN
和Infinity
都是可接受的。其他略。
如果需要串联{}
和[]
,需要加上String()
toString()
JSON.stringify()
等方法。
// @flow
"foo" + String({});
"foo" + [].toString();
"" + JSON.stringify({})
在Flow里,null和void是两种类型,分别处理null
和undefined
可选对象属性可以省略、其他类型,单单不能是null:
// @flow
function acceptsObject(value: { foo?: string }) {
// ...
}
acceptsObject({ foo: "bar" }); // 成功
acceptsObject({ foo: undefined }); // 成功
acceptsObject({ foo: null }); // 错误
acceptsObject({}); // 成功
同上
function method(value: string = "default") { /* ... */ }
可接受void、string、无参数,但不接受null
参数
用来限定参数只能是设定的字面值
// @flow
function getColor(name: "success" | "warning" | "danger") {
switch (name) {
case "success" : return "green";
case "warning" : return "yellow";
case "danger" : return "red";
}
}
getColor("success"); // 成功
getColor("danger"); // 成功
// $ExpectError
getColor("error"); // 错误
设定多种混合类型时,用|
分割
function stringifyBasicValue(value: string | number) { return '' + value;}
function identity<T>(value: T): T { return value;}
function getTypeOf(value: mixed): string { return typeof value;}
在设定mixed
之后,如果要使用传入的值,首先要判断值的类型,否则会报错,Flow有一个完善(refinement)的概念。
// @flowfunction stringify(value: mixed) { // $ExpectError return "" + value; // Error!}stringify("foo");
// @flow
function stringify(value: mixed) {
if (typeof value === 'string') {
return "" + value; // 成功
} else {
return "";
}
}
stringify("foo");
if (typeof value === 'string')
完善value
的类型之后,在if
的上下文内,value
的类型被限定为string
any用于跳出类型检查,原则上是不实用的。
在any类型的参数被引用赋值之后,any类型会泄露到其他变量上,最终会影响到所有代码。
所以在遇到any类型,需要手动完善:
// @flow
function fn(obj: any) {
let foo: number = obj.foo;
}
在类型前加上?
来设定可能类型,可能类型接受本身类型、空值和未定义:
// @flowfunction acceptsMaybeNumber(value: ?number) { // ...}acceptsMaybeNumber(42); // 成功acceptsMaybeNumber(); // 成功acceptsMaybeNumber(undefined); // 成功acceptsMaybeNumber(null); // 成功acceptsMaybeNumber("42"); // 错误
对象属性为可能类型时,对象属性不能不定义。
// @flowfunction acceptsMaybeProp({ value }: { value: ?number }) { // ...}acceptsMaybeProp({ value: undefined }); // 成功acceptsMaybeProp({}); // 错误
完善可能类型,在使用可能类型之前,可以完善使其的类型确定:
方法有 value !== null && value !== undefined
或 value != null
或 typeof value === 'number'
(number为设定的可能类型)
在变量定义中规定类型:
// @flowlet foo = 42;let isNumber: number = foo; // 成功foo = true;let isBoolean: boolean = foo; // 成功 foo = "hello";let isString: string = foo; // 成功
有一些特殊的场景下,Flow的类型判断会失效:
// @flowlet foo = 42;function mutate() { foo = true; foo = "hello";}mutate();// $ExpectErrorlet isString: string = foo; // 错误
函数有两个地方需要设定类型:参数和返回值
function method(str, bool, ...nums) { // ...}function method(str: string, bool?: boolean, ...nums: Array<number>): void { // ...}
let method = (str, bool, ...nums) => { // ...};let method = (str: string, bool?: boolean, ...nums: Array<number>): void => { // ...};
this
函数在JS中每个方法都可以用this
调用,在Flow中,可以在第一个参数设定this
的类型
// @flowfunction method<T>(this: { x: T }) : T { return this.x;}var num: number = method.call({x : 42});var str: string = method.call({x : 42}); // error
在if
条件中的表达式转移到一个新的函数的时候,在函数定义时加上%checks
标注:
function truthy(a, b): boolean %checks { return !!a && !!b;}function concat(a: ?string, b: ?string): string { if (truthy(a, b)) { return a + b; } return '';}
在Flow里有两种对象类型:确切对象类型和非确切对象类型,文档推荐使用确切对象类型。
// @flowvar obj1: { foo: boolean } = { foo: true };var obj2: { foo: number, bar: boolean, baz: string,} = { foo: 1, bar: true, baz: 'three',};
在Flow中,访问一个对象中不存在的属性会报错。
在属性后加?
标注,使该属性可选,该属性值不能为null
对象的方法属性只读。
// @flowlet b = { foo() { return 3; }}b.foo = () => { return 2; } // 错误
对象的方法引用自身时用对象名而非this
// @flow
let a = {
x : 3,
foo() { return this.x; } // 错误
}
let b = {
x : 3,
foo() { return b.x; } // 成功
}
创建一个带有属性的对象之后,Flow将其认作密封对象,对象的属性自带类型。并且无法在密封对象中增加属性
创建一个没有属性的对象之后,Flow将其认作非密封对象,后续可添加属性。
和变量类型表现一致,Flow会赋予属性所有可能的类型:
// @flow
var obj = {};
if (Math.random()) obj.prop = true;
else obj.prop = "hello";
// $ExpectError
var val1: boolean = obj.prop; // Error!
// $ExpectError
var val2: string = obj.prop; // Error!
var val3: boolean | string = obj.prop; // Works!
(React源码中常用)通常传一个有多余属性的对象是可以的,如果需要严格规定传入对象的属性,需要使用确切对象类型:
{| foo: string, bar: number |}
这个时候传递多余的属性就会报错。
(React源码中常用)如果需要结合确切对象的属性,不能用交集,而要用对象属性扩散方法
// @flowtype FooT = {| foo: string |};type BarT = {| bar: number |};type FooBarFailT = FooT & BarT;type FooBarT = {| ...FooT, ...BarT |};const fooBarFail: FooBarFailT = { foo: '123', bar: 12 }; // 错误const fooBar: FooBarT = { foo: '123', bar: 12 }; // 成功
虽然有点绕弯子,但是这样也能定义一个不确切对象类型:
// @flowtype Inexact = {foo: number, ...};
Flow中定义Map对象可以用"索引器属性"定义
// @flowvar o: { [string]: number } = {};o["foo"] = 0;o["bar"] = 1;var foo: number = o["foo"];
索引器属性也可以被命名:
// @flowvar obj: { [user_id: number]: string } = {};obj[1] = "Julia";obj[2] = "Camille";obj[3] = "Justin";obj[4] = "Mark";
let arr: Array<number> = [1, 2, 3];
let arr: number[] = [0, 1, 2, 3];
?
位置不同的不同设定
// @flowlet arr1: ?number[] = null; // Works!let arr2: ?number[] = [1, 2]; // Works!let arr3: ?number[] = [null]; // Error!
// @flowlet arr1: (?number)[] = null; // Error!let arr2: (?number)[] = [1, 2]; // Works!let arr3: (?number)[] = [null]; // Works!
如果访问一个未定义的元素,可能得到undefined
// @flowlet array: Array<number> = [0, 1, 2];let value: number = array[3]; // Works. // ^ undefined
为了避免这种情况,声明变量的时候使用混合类型,并且后续完善。
let array: Array<number> = [0, 1, 2];let value: number | void = array[1];if (value !== undefined) { // number}
和$ReadOnly
类似, $ReadOnlyArray<T>
设定一个只读数组。
$ReadOnlyArray<T>
是所有数组、元组(turple)的超类。
用[type, type, type]
定义Tuple类型
元组的长度被称为 “arity”。元组的长度在Flow中是严格执行的(不可变)。
Tuples与数组类型并不互通。
不能在Tuple上使用数组方法。
类的字段必须在被使用之前设定类型
// @flowclass MyClass { prop: number; method() { this.prop = 42; }}
在类外部定义的字段也必须在类内部设定类型
// @flowfunction func_we_use_everywhere (x: number): number { return x + 1;}class MyClass { static constant: number; static helper: (number) => number; function_property: number => number;}MyClass.helper = func_we_use_everywhereMyClass.constant = 42MyClass.prototype.function_property = func_we_use_everywhere
class MyClass<A, B, C> { property: A; method(val: B): C { // ... }}
//@flowclass MyClass {}(MyClass: MyClass); // Error(new MyClass(): MyClass); // Ok
可以用类型别名复用复杂的类型:
// @flowtype MyObject = { foo: number, bar: boolean, baz: string,};
// @flowtype MyObject = { // ...};var val: MyObject = { /* ... */ };function method(val: MyObject) { /* ... */ }class Foo { constructor(val: MyObject) { /* ... */ } }
type MyObject<A, B, C> = { property: A, method(val: B): C,};
泛类型别名是参数化的,使用时必须传递类型:
// @flowtype MyObject<A, B, C> = { foo: A, bar: B, baz: C,};var val: MyObject<number, boolean, string> = { foo: 1, bar: true, baz: 'three',};
不透明类型别名不允许在别的文件中访问底层类型
opaque type Alias = Type;
export opaque type NumberAlias = number;
import type {NumberAlias} from './exports';(0: NumberAlias) // Error: 0 is not a NumberAlias!function convert(x: NumberAlias): number { return x; // Error: x is not a number!}
在外部文件中使用的时候,不透明类型可作为超类。
export opaque type ID: string = string;
import type {ID} from './exports';function formatID(x: ID): string { return "ID: " + x; // Ok! IDs are strings.}function toID(x: string): ID { return x; // Error: strings are not IDs.}
下述定义方法也成立:
declare opaque type Foo;declare opaque type PositiveNumber: number;
由于类(Class)类型都为定死的,两种结构一致的类类型也不能通用,因此用接口类型可以用相同结构的类类型定义。
// @flowinterface Serializable { serialize(): string;}class Foo { serialize() { return '[Foo]'; }}class Bar { serialize() { return '[Bar]'; }}const foo: Serializable = new Foo(); // Works!const bar: Serializable = new Bar(); // Works!
匿名接口的定义方式:
// @flowclass Foo { a : number}(new Foo() : interface { a : number });
在属性前加上+
interface MyInterface { +readOnly: number | string;}
设定为只读之后,可以传递精确类型的值
// @flowinterface Invariant { property: number | string }interface Covariant { +readOnly: number | string }var x : { property : number } = { property : 42 };var y : { readOnly : number } = { readOnly : 42 };var value1: Invariant = x; // Error!var value2: Covariant = y; // Works
在属性前加上-
interface InterfaceName {
-writeOnly: number;
}
设定为只写之后,可以传递一个不精确类型的值
// @flow
interface Invariant { property: number }
interface Contravariant { -writeOnly: number }
var numberOrString = Math.random() > 0.5 ? 42 : 'forty-two';
// $ExpectError
var value1: Invariant = { property: numberOrString }; // Error!
var value2: Contravariant = { writeOnly: numberOrString }; // Works!
我们可以创建一个泛型类型(或多态类型),代替其他类型使用。
function identity<T>(value: T): T {
return value;
}
//@flowfunction doSomething<T>(param: T): T { // ... return param;}doSomething<number>(3);
//@flowclass GenericClass<T> {}const c = new GenericClass<number>();
也可以只应用其中一部分泛型
//@flowclass GenericClass<T, U, V>{}const c = new GenericClass<_, number, _>()
在泛型中的方法参数设置需要的泛型类型,限定传入的对象:
// @flowfunction logFoo<T: { foo: string }>(obj: T): T { console.log(obj.foo); // Works! return obj;}logFoo({ foo: 'foo', bar: 'bar' }); // Works!// $ExpectErrorlogFoo({ bar: 'bar' }); // Error!
Type1 | Type2 | ... | TypeN
type Foo = | Type1 | Type2 | ... | TypeN
type Numbers = 1 | 2;type Colors = 'red' | 'blue'type Fish = Numbers | Colors;
当调用我们接受联合类型的函数时,我们必须传入这些类型中的一种。但在我们的函数中,我们需要处理所有可能的类型。
使用typeof
完善值类型
// @flowfunction toStringPrimitives(value: number | boolean | string) { if (typeof value === 'number') { return value.toLocaleString([], { maximumSignificantDigits: 3 }); // Works! } // ...}
结合不同属性的对象到一个并查集中(多用于返回对象成功、失败不同的情况)
// @flowtype Success = { success: true, value: boolean };type Failed = { success: false, error: string };type Response = Success | Failed;function handleResponse(response: Response) { if (response.success) { var value: boolean = response.value; // Works! } else { var error: string = response.error; // Works! }}
// @flowtype Success = {| success: true, value: boolean |};type Failed = {| error: true, message: string |};type Response = Success | Failed;function handleResponse(response: Response) { if (response.success) { var value: boolean = response.value; } else { var message: string = response.message; }}
将所有类型结合的类型(React源码中常有)
Type1 & Type2 & ... & TypeN
type Foo = & Type1 & Type2 & ... & TypeN
和并查类型相反
type Fn = & ((x: "string") => string) & ((x: "number") => number) & ((x: string) => null);
这种定义方式每一行都称为重载
如果有相同属性名的相交类型,使用第一个符合的属性类型。
type One = { prop: number };type Two = { prop: boolean };declare var both: One & Two;var prop1: number = both.prop; // okayvar prop2: boolean = both.prop; // Error: number is incompatible with boolean
相交类型允许结合相互矛盾的类型:
// @flowtype NumberAndString = number & string;function method(value: NumberAndString) { // ...}// $ExpectErrormethod(3.14); // Error!// $ExpectErrormethod('hi'); // Error!
允许你访问到对象、数组、tuple的属性类型。
// @flow
type Cat = {
name: string,
age: number,
hungry: boolean,
};
type Hungry = Cat['hungry']; // type Hungry = boolean
const isHungry: Hungry = true; // OK - `Hungry` is an alias for `boolean`
// The index can be a type, not just a literal:
type AgeProp = 'age';
type Age = Cat[AgeProp]; // type Age = number
const catAge: Age = 6; // OK - `Age` is an alias for `number`
索引也可以是一个联集
// @flow
type Cat = {
name: string,
age: number,
hungry: boolean,
};
type Values = Cat[$Keys<Cat>]; // type Values = string | number | boolean
也可以是泛型
function getProp<O: {+[string]: mixed}, K: $Keys<O>>(o: O, k: K): O[K] { return o[k];}const x: number = getProp({a: 42}, 'a'); // OKconst y: string = getProp({a: 42}, 'a'); // Error - `number` is not a `string`getProp({a: 42}, 'b'); // Error - `b` does not exist in object type
function getProp<O: {+[string]: mixed}, K: $Keys<O>>(o: O, k: K): O[K] { return o[k];}const x: number = getProp({a: 42}, 'a'); // OKconst y: string = getProp({a: 42}, 'a'); // Error - `number` is not a `string`getProp({a: 42}, 'b'); // Error - `b` does not exist in object type
// @flowlet obj1 = { foo: 1, bar: true, baz: 'three' };let obj2: typeof obj1 = { foo: 42, bar: false, baz: 'hello' };let arr1 = [1, 2, 3];let arr2: typeof arr1 = [3, 2, 1];
断言与投递到不同的类型
(value: Type)
只要是表达式能出现的地方就能使用类型投递表达式
let val = (value: Type);let obj = { prop: (value: Type) };let arr = ([(value: Type), (value: Type)]: Array<Type>);
可以判定值的类型
// @flow
let value = 42;
(value: 42); // Works!
(value: number); // Works!
(value: string); // Error!
表达式结果的值类型会投递给新的值
// @flow
let value = 42;
(value: 42); // Works!
(value: number); // Works!
let newValue = (value: number);
// $ExpectError
(newValue: 42); // Error!
(newValue: number); // Works!
遇到$
开头的时候查阅https://flow.org/en/docs/types/utilities/
// @flow
export default class Foo {};
export type MyObject = { /* ... */ };
export interface MyInterface { /* ... */ };
// @flow
import type Foo, {MyObject, MyInterface} from './exports';
// @flow
const myNumber = 42;
export default myNumber;
export class MyClass {
// ...
}
// @flow
import typeof myNumber from './exports';
import typeof {MyClass} from './exports';
基于注释,从而不用编译文件,直接在原生JS文件中使用Flow
// @flow
/*::
type MyAlias = {
foo: number,
bar: boolean,
baz: string,
};
*/
function method(value /*: MyAlias */) /*: boolean */ {
return value.bar;
}
method({ foo: 1, bar: true, baz: ["oops"] });
function method(param /*: string */) /*: number */ {
// ...
}
function method(param /*:: : string */) /*:: : number */ {
// ...
}