wgao19 / flow-notes

πŸ“ Notes on using and understanding Flow

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Flow Notes

πŸ“ Notes on using and understanding Flow, starting from version 0.85.

🚧 This repo is a work in progress.

Table of contents

(you should ignore this part and create your own)


Learning Flow

授人δ»₯ι±ΌδΈε¦‚ζŽˆδΊΊδ»₯ζΈ”

This section is a work in progress

Learn Flow usages

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:

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 with mapState, 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

Videos

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

Even more


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

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:

Functions

https://flow.org/en/docs/types/functions/

The docs cover basics of functions such as:

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:

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
  }
}

Flow Try.

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

Flow Try

Transported from an example in the book Programming TypeScript.

Try Flow bookmarklets

Guides

About

πŸ“ Notes on using and understanding Flow

License:MIT License