当前位置: 首页 > 工具软件 > React Flow > 使用案例 >

【Flow】快速入门Flow.js 类型注释 #React源码基础知识

桓智敏
2023-12-01

快速入门Flow.js 类型注释

前言

最近在看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会报错。

类型注释

本节将简明给出所有类型的注释方法。

原始类型

原始类型包括:

  • Booleans
  • Strings
  • Numbers
  • null
  • undefined (void in Flow types)
  • Symbols (new in ECMAScript 2015)

原始类型有两种:

  • 字面值

    true;
    "hello";
    3.14;
    null;
    undefined;
    
  • 包装器对象

    new Boolean(false);
    new String("world");
    new Number(42);
    

在Flow中声明原始值分为小写(字面值)和首字母大写(包装器对象)。

Booleans

声明为boolean类型之后,只接受true, false 以及表达式判断。

// @flow
function acceptsBoolean(value: boolean) {
  // ...
}

acceptsBoolean(true);  		// 成功
acceptsBoolean(false); 		// 成功
acceptsBoolean(0);          // 错误
acceptsBoolean("foo");      // 错误
acceptsBoolean(Boolean(0)); // 成功
acceptsBoolean(!!0);        // 成功

Numbers

需要注意的是,NaNInfinity都是可接受的。其他略。

Strings

如果需要串联{}[],需要加上String() toString() JSON.stringify()等方法。

// @flow
"foo" + String({});     
"foo" + [].toString(); 
"" + JSON.stringify({})

null和void

在Flow里,null和void是两种类型,分别处理nullundefined

可选对象属性

可选对象属性可以省略、其他类型,单单不能是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");   // 错误

混合类型(mixed)

设定多种混合类型时,用|分割

多种可能类型

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类型的参数被引用赋值之后,any类型会泄露到其他变量上,最终会影响到所有代码。

所以在遇到any类型,需要手动完善:

// @flow
function fn(obj: any) {
  let foo: number = obj.foo;
}

可能类型(Maybe)

在类型前加上?来设定可能类型,可能类型接受本身类型、空值和未定义:

// @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 !== undefinedvalue != nulltypeof 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, ...};

定义Map对象

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)的超类。

Tuple类型

[type, type, type]定义Tuple类型

元组的长度被称为 “arity”。元组的长度在Flow中是严格执行的(不可变)。

Tuples与数组类型并不互通。

不能在Tuple上使用数组方法。

类(Class)类型

类的字段必须在被使用之前设定类型

// @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.}

依赖库(library)定义

下述定义方法也成立:

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!

联合(Union)类型

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!  }  // ...}

并查类型(Disjoint Union)

结合不同属性的对象到一个并查集中(多用于返回对象成功、失败不同的情况)

// @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;  }}

相交(Intersection)类型

将所有类型结合的类型(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

Typeof类型

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!

工具(Utility)类型

遇到$开头的时候查阅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 */ {
  // ...
}
 类似资料: