使用flow的工具类型

因为flow的类型本身是固定的,如果想要对类型进行转换和操作,就得借助于工具类型(Utility Types),有了它们,我们可以更加灵活地使用flow。

类型

$Keys

$Keys顾名思义,是用于获取一个类型的键值,类似与Object.keys。注意$Keys<T>返回的是一个联合类型(Union Type),而不是数组。

1
2
3
4
5
6
7
8
9
10
// @flow

const gender = {
man: 1,
woman: 0,
}

function doGender(genderStr: 'man' | 'woman'){
//...
}

上面创建一个gender的对象(类似于枚举),如果有个方法需要加入性别文字的类型声明,就需要添加枚举对象中的每一个键值,这个显然是很笨拙的方式。这时$Keys就派上用场了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

const gender = {
man: 1,
woman: 0,
}

function doGender(genderStr: $Keys<typeof gender>){
//...
}

doGender('man'); // Works!
doGender('woman'); // Works!

$Values

了解了$Keys$Values也变得简单了,接着上面的示例,如果我们的函数接受的是枚举值,而不是键值文本时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @flow

const gender = {
man: 1,
woman: 0,
}

function doGender(genderStr: $Values<typeof gender>){
//...
}

doGender(0); // Works!
doGender(1); // Works!
doGender(2); // Works!

等等,细心一点就会发现,doGender(2)怎么也没有抛错呢!

让我们回忆一下上文中关于typeof类型的一个示例。

值得注意的是,如果对文字没有显式声明类型,flow会将其推导成基本类型,所以typeof得出的也是基本类型。但是如果对文字显式声明了文字类型,typeof就会得到显式声明的类型。

由于没有对gender显式声明类型,内部的字段都被推导成基本类型了,所以我们要加以改造。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @flow

typeof Gender = {
man: 1,
woman: 0,
}

const gender: Gender = {
man: 1,
woman: 0,
}

function doGender(genderStr: $Values<typeof gender>){
//...
}

doGender(0); // Works!
doGender(1); // Works!
doGender(2); // Error!

显式的声明gender的类型之后,doGener如预期的抛出了错误。

$ReadOnly

$ReadOnly正如其名是将一个对象类型的字段变成只读。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

type Obj = {
key: number,
}

type ReadOnlyObj = $ReadOnly<Obj>;

const obj: ReadOnlyObj = {
key: 10
}

obj.key = 11; // Error!

等价于

1
2
3
4
5
// @flow

type Obj = {
+key: number,
}

$Exact

$Exact<{name: string}> 等价于 {| name: string |},用于严格限制对象属性。

$Diff<A, B>

$Diff<A, B>返回一个A中存在但B中不存在的属性,相当于A / B。注意如果B中包含A中没有的属性,那么$Diff会抛出错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

type A = {
x: number,
y: number,
}

type B = {
y: number,
z: number,
}

$Diff<A, B> // Error!

现在我们看一个正确的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @flow

type Props = {
x: number,
y: number,
}

type DefaultProps = {
y: number,
}

type RequiredProps = type $Diff<Props, DefaultProps>;

function Comp(props: RequiredProps){}

看起来是不是有点眼熟,没错,这就是React的定义文件处理props的方式。

如果B中某个属性在A中不存在,但这个属性是可选参数,那么也是可以的。

1
2
type A = $Diff<{}, {nope: number}>; // Error
type B = $Diff<{}, {nope: number | void}>; // OK

$Rest<A, B>

在es6中新增了Object Spread语法,如

1
2
3
4
5
6
7
const obj = {
x: 1,
y: 2,
z: 3,
}

const {x, ...rest} = obj;

$Rest<A, B>与其类似,AB都是Object类型

1
2
3
4
5
6
7
// @flow

type Props = { name: string, age: number };

type RestProps = $Rest<Props, { name: string }>

({age:1}: RestProps);

表面上看$Rest<A, B>$Diff<A, B>功能是差不多的,实际上它们的区别在于对对象自身的属性的处理。

1
2
3
4
5
6
7
// @flow
type Props = {| name: string, age: number |};

const props: Props = {name: 'Jon', age: 42};
const {age, ...otherProps} = props;
(otherProps: $Rest<Props, {|age: number|}>);
otherProps.age; // Error

先看看这个示例,对他们之间的区别应该会有个大概的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent(){
this.a = 'a';
}

function Child(){
this.b = 'b';
}

Child.prototype = new Parent;

const child = new Child();

const {b,...rest} = child;

console.log(child.a); // a

console.log(rest.a); // undefined

console.log(rest.hasOwnProperty('a')) // false

可以看到Object Spread并没有处理原型上的数据,仅是对字面量结构进行了展开,也就是说只会处理对象自身的属性。

flow会将声明成严格匹配对象类型(Exact Object Type)的属性当做是ownProps(对象自身的属性)。

所以flow的为了描述真实的Object Spread操作,需要明确的限定这个对象类型的边界,否则flow会认为这个对象可能存在其他属性。举个示例,由于{}中仍然可能有n属性,所以$Rest<{|n: number|}, {}>会返回{|n?: number|},而$Diff<{|n: number|}, {}>会返回{|n: number|}

同时我们还必须指明被展开对象类型是严格匹配对象类型,否则会造成展开后对象上的属性丢失。例如 $Rest<{n: number}, {||}>会返回{|n?: number|},因为n属性可能存在于对象的原型上,展开后在剩余属性上就并不存在。

$PropertyType<T, k>

此工具类型用于获取对象类型的一个属性的类型。k必须是一个文本字符串。

1
2
3
4
5
6
7
8
9
10
// @flow
type Person = {
name: string,
age: number,
parent: Person
};

const newName: $PropertyType<Person, 'name'> = 'Michael Jackson'; // OK
const newAge: $PropertyType<Person, 'age'> = 50; // OK
const newParent: $PropertyType<Person, 'parent'> = 'Joe Jackson'; // Error! parent是Person类型,而传入的是字符串

在react中获取props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @flow

import React from 'react';
class Tooltip extends React.Component {
props: {
text: string,
onMouseOver: ({x: number, y: number}) => void
};
}

const someProps: $PropertyType<Tooltip, 'props'> = {
text: 'foo',
onMouseOver: (data: {x: number, y: number}) => undefined
};

它还支持嵌套操作:

1
2
3
//...

type PositionHandler = $PropertyType<$PropertyType<Tooltip, 'props'>, 'onMouseOver'>;

如果想要获取类的静态属性,可以使用Class<T>

1
2
3
4
5
6
7
8
// @flow
class BackboneModel {
static idAttribute: string | false;
}

type ID = $PropertyType<Class<BackboneModel>, 'idAttribute'>;
const someID: ID = '1234';
const someBadID: ID = true;

$ElementType<T, K>

这个工具也是用来获取T的属性的类型,和$PropertyType不同点在于参数K可以为声明类型,而不只是字符串文本,并且它可以作用于arraytupleobject类型。

1
2
3
4
5
6
7
8
9
10
11
12
// @flow

type Obj = { [key: string]: number };
(42: $ElementType<Obj, string>);
(42: $ElementType<Obj, boolean>); // Error! object keys aren't booleans
(true: $ElementType<Obj, string>); // Error! elements are numbers

// 虽然不知道数组的长度,但是我们可以用number作为下标
type Arr = Array<boolean>;
(true: $ElementType<Arr, number>);
(true: $ElementType<Arr, boolean>); // Error! 数组下标不为boolean
('foo': $ElementType<Arr, number>); // Error! 'foo'类型不为number

$ElementType更强大的地方在于可以K可以传入泛型(generics):

1
2
3
4
5
6
7
8
9
// @flow

function getProp<O: {+[string]: mixed}, P: $Keys<O>>(o: O, p: P): $ElementType<O, P> {
return o[p];
}

(getProp({a: 42}, 'a'): number); // OK
(getProp({a: 42}, 'a'): string); // Error!
getProp({a: 42}, 'b'); // Error!

$NonMaybeType

顾名思义,此工具用于将一个Maybe Type转换成非Maybe Type

1
2
3
4
5
6
7
8
9
// @flow

type MaybeName = ?string;
type Name = $NonMaybeType<MaybeName>;

('Gabriel': MaybeName); // Ok
(null: MaybeName); // Ok
('Gabriel': Name); // Ok
(null: Name); // Error!

$ObjMap<T, F>

$ObjMap<T, F>传入一个对象类型T和一个泛型函数类型F,返回一个新的对象类型,新对象类型的键和T一样,而键值是F的返回值。F会传入了键值的类型V作为泛型参数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//@flow

// 注意ExtractReturnType没有<T>
type ExtractReturnType = <V>(() => V) => V

function run<O: {[key: string]: Function}>(o: O): $ObjMap<O, ExtractReturnType> {
return Object.keys(o).reduce((acc, k) => Object.assign(acc, { [k]: o[k]() }), {});
}

const o = {
a: () => true,
b: () => 'foo'
};

(run(o).a: boolean); // Ok
(run(o).b: string); // Ok
// $ExpectError
(run(o).b: boolean); // Nope, b is a string
// $ExpectError
run(o).c;

$TupleMap<T, F>

上面是对象,而这个是用来操作可迭代类型TupleArray,功能类型与javascript中数组的map方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

// 注意ExtractReturnType没有<T>
type ExtractReturnType = <V>(() => V) => V

function run<A, I: Array<() => A>>(iter: I): $TupleMap<I, ExtractReturnType> {
return iter.map(fn => fn());
}

const arr = [() => 'foo', () => 'bar'];
(run(arr)[0]: string); // OK
(run(arr)[1]: string); // OK
(run(arr)[1]: boolean); // Error

$Call

$Call<F>用于调用一个函数类型,返回函数类型的返回类型。通过它我们可以在运行时创建多种不同的类型,而不需要一个个写静态类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

function getFirstValue<V>(map: Map<string, V>): ?V {
for (const [key, value] of map.entries()) {
return value;
}
return null;
}

type Value = $Call<typeof getFirstValue, Map<string, number>>;

(5: Value);
(true: Value); // Error!

#Class

上面已经见过了,用于获取一个Class类型的类型。如果一个变量是类本身,就需要使用Class<T>来声明,因为直接用类来声明的是类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
// @flow

class ParamStore<T> {
constructor(data: T) {}
}

function makeParamStore<T>(storeClass: Class<ParamStore<T>>, data: T): ParamStore<T> {
return new storeClass(data);
}

(makeParamStore(ParamStore, 1): ParamStore<number>);
(makeParamStore(ParamStore, 1): ParamStore<boolean>); // Error!

$Shape

复制对象类型对象的属性,并将其设置成可选类型(注意不是Maybe Type)。

1
2
3
4
5
6
7
8
9
10
11
12
// @flow
type Person = {
age: number,
name: string,
}
type PersonDetails = $Shape<Person>;

const person1: Person = {age: 28}; // Error: missing `name`
const person2: Person = {name: 'a'}; // Error: missing `age`
const person3: PersonDetails = {age: 28}; // OK
const person4: PersonDetails = {name: 'a'}; // OK
const person5: PersonDetails = {age: 28, name: 'a'}; // OK

Existential Type (*)

*any不同,*会告诉flow去推导这个类型,所以在一定程度上可以避免丢失类型安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// @flow

class DataStore {
data: *; // If this property weren't defined, you'd get an error just trying to assign `data`
constructor() {
this.data = {
name: 'DataStore',
isOffline: true
};
}
goOnline() {
this.data.isOffline = false;
}
changeName() {
this.data.isOffline = 'SomeStore'; // oops, wrong key!
}
}

记住*并不是总能推导出类型,如果上下文无法推断,那么*就和any无异。

$Subtype 和 $Supertype

虽然在官方文档中显示为Work in Progress,其实这两个工具在很多模块已经在使用了。

在弄清SubtypeSupertype的概念之前,先把它俩搁置一下吧。

结语

参考文档: