flow-notes
flow-notes copied to clipboard
📝 Notes on using and understanding Flow
Flow Notes
📝 Notes on using and understanding Flow, starting from version 0.85.
🚧 This repo is a work in progress.
Table of contents
Part I – Learning Flow
-
Linked usages from tests
- Flow's tests as examples
- Flow Typed tests as examples
- Understanding library definitions
- Learning materials
- Advanced
Part II – Study notes
(you should ignore this part and create your own)
-
Basics
- Objects
- Functions
-
Usages with React
- React components (work in progress)
- Higher order components
- Annotating connected (with Redux) components
-
Advanced topics
- Tagging
Learning Flow
授人以鱼不如授人以渔
This section is a work in progress
Learn Flow usages
Flow's tests as examples
This section is a work in progress
Flow Typed tests as examples
This section is a work in progress
Understanding library definitions
To understand library definitions, first you'll need to understand the following concepts well, here are two articles that explain them:
- variance: https://medium.com/@forbeslindesay/covariance-and-contravariance-c3b43d805611
- overloading: https://github.com/wgao19/flow-notes/blob/master/advanced/callable-properties-and-function-statics.md
Inside library definitions are type declarations. And specifically, they normally include:
- functions
- classes
- type aliases
What's not in the library definition files are the actual implementations of them. If you've taken CS classes, think about header files in C++.
To list a few examples from the libdefs of the libraries that we use:
What those libdefs normally look like is that
- very abbreviated function type parameters – we'd have to bear with it. They normally provide a dictionary in the beginning so we can use as reference.
- multiple call signatures of functions and classes – this is understandable, because most libraries have multiple ways of calling function or constructors. i.e., React Redux's
connect
can be called withmapState
,mapDispatch
, both, or even more ways. Furthermore, depending on how the functions are called, they may return different results. Both of the above may be achieved with overloading, as you may read more in this note. - each call signatures are heavily marked by variance sigils – this is a powerful feature by Flow, it allows users to mark whether subtypes / supertypes may count as valid arguments supplied to the function parameters. If you don't like the concepts of variance, you can think of the sigils (the
+
s and-
s) as read-only v.s. write-only, as well explained in this article.
Understanding Flow
- Flow's official blog and a list of well-written articles:
- Coming Soon: Changes to Object Spreads you must read this
- typescript-vs-flowtype
Videos
- Flow Reloaded New Challenges and New Opportunities
- Jeff Morrison - A Deepdive Into Flow at react-europe 2016
- Flow: Static Type Systems At Scale
Books
- Programming TypeScript A practical handbook on TypeScript that also explains the whys and hows behind static type checking well, see also swyx's recommendation tweet
Advanced
Books
- Types and Programming Languages has a very heavy math start
- Advanced Topics in Types and Programming Languages
Even more
- Pottier's course and thesis (GitLab repo)
- The Essence of ML Type Inference (paper)
Study notes
you should ignore this part and create your own
Basics
https://flow.org/en/docs/lang/
Objects
https://flow.org/en/docs/types/objects/
Familiar yourself with objects by reading the docs. It covers such questions as
- Basic syntax for typing objects
- Optional and nullable properties
- What are sealed objects?
- What are unsealed objects?
- What are exact objects?
- How to use indexer properties?
-
What are the differences among
{}
,Object
, and{ [key: string]: any }
?
Examples (Flow Try)
type Name =
| "Simba"
| "Mufasa"
| "Nala"
| "Sarabi"
| "Sarafina"
| "Scar"
| "Kiara"
| "Kovu"
| "Vitani";
const GENDER = {
MALE: "male",
FEMALE: "female"
};
const purrs = {
brief: "prr",
normal: "prrr",
long: "prrrrr",
insane: "prrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"
};
type Kitten = {| // recommend using sealed and exact objects
/** basic fields */
name1: string,
name2: // disjoint union for more refined typing
| "Simba"
| "Mufasa"
| "Nala"
| "Sarabi"
| "Sarafina"
| "Scar"
| "Kiara"
| "Kovu"
| "Vitani",
name3: Name, // type alias for easier reuse and readability
age: number,
gender: $Values<typeof GENDER>, // 'male' | 'female'
purrs: $Keys<typeof purrs>, // 'brief' | 'normal' | 'long' | 'insane'
/** function fields */
purr1: Function, // not recommended because Function is now aliased to `any`
purr2: () => {}, // likely a typo, did you mean () => void?
purr3: () => void, // (very common) a function that doesn't take nor return anything
purr4: string => void, // (very common)
purr5: (name: string) => boolean, // (very common) use named parameter for better readability
/** object fields */
mane1: Object, // not recommended
mane2: {}, // not recommended: not even sealed, nearly the same as any
mane3?: { // optional field, meaning this field may or may not exist
[key: string]: any // can be a last resort if you really don't know what's going on
},
mane4: ?{ // nullable, meaning this field can be null or undefined
color: string, // (very common) with defined properties
type: "mane" | "beard" | "mustache"
},
manes?: ?({ // fields can be both optional and nullable
color: string,
type: "mane" | "beard" | "mustache"
}[]) // array of objects
|};
const kitten: Kitten = {
name1: 'kitten',
name2: 'Simba',
name3: 'Nala',
age: 7,
gender: 'male', // note that this type is widened
purrs: 'brief', // must be one of the literals
purr1: () => { console.log('prr') },
purr2: () => ({}),
purr3: () => { console.log('prr') },
purr4: name => { console.log(name + ', prrrrr') },
purr5: name => !!name,
mane1: {},
mane2: {},
mane3: { color: 'darkbrown' },
mane4: { color: 'darkbrown', type: 'beard' },
manes: [{ color: 'darkbrown', type: 'mustache' }]
};
We will proceed to discuss a few behaviors of objects.
Exactness vs sealed when passing objects to functions
Exact means the object passed in here must not contain extra fields (link to doc).
Sealed, on the other hand, means the object may contain other fields but you may not access them unless you annotate them. When we write an object type with some fields, it is by default sealed. Trying to access unannotated field of a sealed object will result in error, regardless of it being exact or not:
type Sealed = { foo: string }
const sealed = {
foo: 'foo',
}
sealed.bar = 'bar' // error
const { foo, bar } = sealed // error
For functions that expect sealed objects, you can still pass in objects with extra props:
function usingSealed(x: Sealed) {
// does things
}
usingSealed({ foo: 'foo', bar: 'bar' }) // ok
Not so when the functions are expecting exact objects:
type Exact = {| foo: string |}
function usingExact(x: Exact) {
// does things
}
usingExact({ foo: 'foo', bar: 'bar' }) // error
We may access the fields of function parameters by destructuring the object:
// destructuring on function parameter
function goodUsingSealed({ foo }: Sealed) { // ok
// does things
}
But since destructuring means accessing the object, we are unable to access extra props to sealed objects (relies on a fix from v0.100):
// fixed in 0.100
function badUsingSealed({ foo, bar }: Sealed) { // error after 0.100
// does things
}
Disjoint union
This section is currently under work in progress.
Intersection
This section is currently under work in progress.
Spreading inexact objects
Because non-exact objects can have any properties other than defined, spreading objects yields the following behavior which may be counter-intuitive (but is reasonable with a bit of thoughts)
- properties from spread becomes optional → because currently only "own" properties are copied
- properties before spread becomes mixed → because the incoming inexact objects may contain more properties that may overwrite existing properties
type A = {
a: number
}
type B = {
b: number,
...A
}
// The same as
type B = {
a?: number, // every property from spread becomes optional
b: mixed // every property before non-exact spread becomes mixed
};
To avoid surprises, always spread exact objects.
To avoid surprises, always spread exact objects:
type Appearance = {| // <- exact type
eyes: 'black' | 'brown',
hair: 'chestnut' | 'coal',
|};
type Feature = { // <- not exact
purrs: 'does-not-purr' | 'brief'
}
type MyKitty = {
...Appearance,
...$Exact<Feature>, // <- marks all Feature properties exact
}
Note
After Flow v0.106, the two phenomena above are re-addressed differently:
- the new model assumes inexact object types specify own-ness on specified properties, therefore spread properties will no longer be made optional, following the run time object spread more intuitively
- properties before spread becomes mixed – because the incoming inexact objects may contain more properties that may overwrite existing properties, the new implementation will err, tell us what happens, and ask if we can make the incoming object exact
More about this:
- Coming Soon: Changes to Object Spreads Flow's blog article on object spread
- my TL;DR on the article above
- facebook/flow#3534
Functions
https://flow.org/en/docs/types/functions/
The docs cover basics of functions such as:
- Function parameters (input) and return value (output) are often optional
- There are three different syntaxes to annotate functions: function declarations, arrow functions, and function types
- Syntaxes for annotating function parameters, optional parameters, and rest parameters
- Return types ensure that every branch of your function returns the same type
- What is a predicate function?
-
Function
is now aliased toany
and is therefore not recommended
Examples (Flow Try)
/** functions with no parameters nor returns */
function purr1(): void { // function declaration
console.log('purr');
}
const purr2 = (): void => { // arrow function
console.log('purr');
}
type PurrA = () => void;
const purr3: PurrA = () => {
console.log('purr');
}
/** functions with parameters and / or returns */
function purr4(name: string): string {
return 'hello, ' + name;
}
const purr5 = (name: string): string => {
return 'hello, ' + name;
}
type PurrB = string => string; // you may optionally leave out parameter names
const purr6:PurrB = name => 'hello, ' + name;
/** polymorphic functions / generics */
function purrAt<T: { name: string }>(creature: T): string {
return 'hello, ' + creature.name;
}
purrAt({ namee: 'uhuh' }); // sticky keyboard error
/** functions with statics */
type PurrMemo = {
cachedName: string,
[[call]](name: string): string, // callable signature
}
const purrMemo: PurrMemo = (name: string) => {
if (!purrMemo.cachedName) {
purrMemo.cachedName = name;
}
return purrMemo.cachedName;
}
/** overloading */
type PurrWithAttitudes = {
(): string,
(name: string): string,
(name: string, times: number): string,
}
const purrWithAttitudes: PurrWithAttitudes = (name?: string, times?: number) => {
if (!name) {
return 'no hello';
} else if (!times) {
return 'hello, ' + name;
} else {
let count = 0, greeting = 'hello';
while (count < times) {
count++;
greeting += ' hello';
}
return greeting + ', ' + name;
}
}`
Usages with React
Once again, familiarize yourself with usages with React described in the docs, here is a list of some essential questions answered there:
- How to represent any React node?
- How to represent an instance of certain component type?
- What's the most abstract representation of a React component and what is it mostly used for?
To use Flow with React, first import React as default exports:
import * as React from 'react';
This way, React will contain the necessary type information exported from the library definitions.
We introduce two common React components here.
React.AbstractComponent
This section is currently under work in progress.
Higher order components
Note
Before going into this section, note that with React Hooks, higher order components may no longer be a preferred pattern. You should try using hooks first. And if you are at the unfortunate position where you have to annotate higher order components, such as working with legacy code, etc., the following section may be helpful.
Annotating the hoc
// makeUnicorn.js
import * as React from 'react';
export type UnicornProps = {|
decoration: string,
|};
export default function makeUnicorn<P: UnicornProps>
(Creature: React.AbstractComponent<P>):
React.AbstractComponent<$Diff<P, UnicornProps>> {
return (props) => <Creature {...props} decoration="spiraling-horn" />;
}
When using the higher order component, you can import the type of the props injected by this higher order component:
// kittenCorn.js
import * as React from 'react';
import makeUnicorn from './makeUnicorn';
import type { UnicornProps } from './makeUnicorn';
type KittenCornProps = {|
name: string,
|} & UnicornProps;
const KittenCorn = ({ name, decoration }) => <div>
{name}
{decoration}
</div>
export default makeUnicorn(KittenCorn);
When we instantiate KittenCorn
, we no longer need to provide the decoration
prop.
render(<KittenCorn name="meow" />);
Annotating components wrapped by hoc by explicitly providing type parameters
// kittenCorn.js
import * as React from 'react';
import makeUnicorn from './makeUnicorn';
import type { UnicornProps } from './makeUnicorn';
type KittenCornProps = {|
name: string,
|} & UnicornProps;
const KittenCorn = ({ name, decoration }: KittenCornProps) => <div>
{name}
{decoration}
</div>
export default makeUnicorn<KittenCornProps>(KittenCorn);
Annotating components wrapped by hoc by casting at export
// kittenCorn.js
import * as React from 'react';
import makeUnicorn from './makeUnicorn';
import type { UnicornProps } from './makeUnicorn';
type KittenProps = {|
name: string,
|};
const KittenCorn = (
{ name, decoration }: KittenProps & UnicornProps
) => <div>
{name}
{decoration}
</div>
export default (
makeUnicorn(KittenCorn): React.AbstractComponent<KittenProps>
);
Dealing with nested higher order components
// gloryKittenCorn.js
import * as React from 'react';
import { compose } from 'redux'; // ¯\_(ツ)_/¯
import makeUnicorn from './makeUnicorn';
import type { UnicornProps } from './makeUnicorn';
import glorifyMane from './glorifyMane';
import type { ManeProps } from './glorifyMane';
type KittenProps = { // OwnProps
name: string
}
const GloryKittenCorn = (
{ name, decoration, mane }: KittenProps & ManeProps & UnicornProps
) => <React.Fragment>
{ name }
{ decoration }
{ mane }
</React.Fragment>;
export default (
compose(
makeUnicorn,
glorifyMane,
)(KittenCorn): React.AbstractComponent<KittenProps>
)
Note
Spreading is tricky in Flow. To avoid complexity, using exact objects for component props is highly recommended.
Links
Annotating connected (with Redux) components
To annotate React Redux's connected components, first be familiar with annotating higher order components using React.AbstractComponent
(discussed in the previous section).
Connecting stateless component with mapStateToProps
type OwnProps = {| // use exact object for component props
passthrough: number,
forMapStateToProps: string,
|};
type Props = {|
...OwnProps,
fromStateToProps: string
|};
const Com = (props: Props) => <div>{props.passthrough} {props.fromStateToProps}</div>
type State = {a: number};
const mapStateToProps = (state: State, props: OwnProps) => {
return {
fromStateToProps: 'str' + state.a
}
};
const Connected = connect<Props, OwnProps, _, _, _, _>(mapStateToProps)(Com);
export default connect()(MyComponent);
Connecting components with mapDispatchToProps
of action creators
type OwnProps = {| // use exact object for component props
passthrough: number,
|};
type Props = {|
...OwnProps,
dispatch1: (num: number) => void,
dispatch2: () => void
|};
class Com extends React.Component<Props> {
render() {
return <div>{this.props.passthrough}</div>;
}
}
const mapDispatchToProps = {
dispatch1: (num: number) => {},
dispatch2: () => {}
};
const Connected = connect<Props, OwnProps, _, _, _, _>(null, mapDispatchToProps)(Com);
e.push(Connected);
<Connected passthrough={123} />;
Connecting components with mapStateToProps
and mapDispatchToProps
of action creators
type OwnProps = {| // use exact object for component props
passthrough: number,
forMapStateToProps: string
|};
type Props = {|
...OwnProps,
dispatch1: () => void,
dispatch2: () => void,
fromMapStateToProps: number
|};
class Com extends React.Component<Props> {
render() {
return <div>{this.props.passthrough}</div>;
}
}
type State = {a: number}
type MapStateToPropsProps = {forMapStateToProps: string}
const mapStateToProps = (state: State, props: MapStateToPropsProps) => {
return {
fromMapStateToProps: state.a
}
}
const mapDispatchToProps = {
dispatch1: () => {},
dispatch2: () => {}
};
const Connected = connect<Props, OwnProps, _, _, _, _>(mapStateToProps, mapDispatchToProps)(Com);
Annotating nested higher order components with connect
If you are at the unfortunate position where your component is wrapped with nested higher order component, it is probably more difficult to annotate by providing explicit type parameters, as doing so will probably require that you tediously take away props at each layer. It is again easier to annotate at function return:
type OwnProps = {|
passthrough: number,
forMapStateToProps: string,
|}
type Props = {|
...OwnProps,
injectedA: string,
injectedB: string,
fromMapStateToProps: string,
dispatch1: (number) => void,
dispatch2: () => void,
|}
const Component = (props: Props) => { // annotate the component with all props including injected props
/** ... */
}
const mapStateToProps = (state: State, ownProps: OwnProps) => {
return { fromMapStateToProps: 'str' + ownProps.forMapStateToProps },
}
const mapDispatchToProps = {
dispatch1: number => {},
dispatch2: () => {},
}
export default (compose(
connect(mapStateToProps, mapDispatchToProps),
withA,
withB,
)(Component): React.AbstractComponent<OwnProps>) // export the connected component without injected props
Advanced topics
Tagging
Consider we have a function that takes an object that takes either a value or a function that generates a value. If the input is a generator, we run the generator to get the intended value. This is quite common as we often consider actions and action generators to produce the same semantic meaning.
function consumesAction<T>(action: T | () => T) {
if (typeof action === 'function') {
console.log(action());
// does other things
} else {
console.log(action);
// does other things
}
}
This is because, T
as an implicit type generic may itself be a function. And knowing that action is function does not align that with the branch that it is the generator function that yields T
.
In this case, consider "tagging" the two branches using disjoint union:
type ActionOrGenerator<A> =
| {tag: 'action', value: A}
| {tag: 'generator', value: () => A}
function consumesAction<Action>(unicorn: ActionOrGenerator<Action>) {
switch (action.tag) {
case 'action':
console.log(action.value)
break
case 'generator':
console.log(action.value())
}
}
(Courtesy of this comment by Yawar Amin.)
Tagging is a common technique that can be used to carry over explicit information cross objects.
Consider this case where we have a function that handles two possible types of mysteries:
type StringType = { targetType: string, target: 'its a string!' };
type NumberType = { targetType: number, target: 0 };
function handle(mystery: StringType | NumberType) {
if (typeof mystery.targetType === 'string') {
const presumablyAString: string = mystery.target;
} else {
const presumablyANumber: number = mystery.target;
}
}
It will err because the branched information on targetType
cannot be carried over to target
. To achieve the desired type, once again, we may tag the branches with disjoint unions:
type TaggedStringType = { tag: 'string', targetType: string, target: 'its a string!' };
type TaggedNumberType = { tag: 'number', targetType: number, target: 0 };
function handle(mystery: TaggedStringType | TaggedNumberType) {
if (mystery.tag === 'string') {
const presumablyAString: string = mystery.target;
} else {
const presumablyANumber: number = mystery.target;
}
}
Transported from an example in the book Programming TypeScript.
Try Flow bookmarklets
-
React.Config
use case - Annotating memoized factorial function
- Cannot write overloaded function bodies
- Cannot call function that takes a refined type even if the extra field is optional
- Cannot properly refine "T or generates T" type
- Spread v.s. & @nutstick
Guides
- Upgrading Flow past 0.85, annotating connect