Flow static type checker is a wonderful attempt to bring algebric data types to JS. It is still fairly new project and there for has few WTFs that can pull you down the rabbit hole. This document is attempt to document things that may seem like a WTF from the perspective of JS developer who tries to employ static type checker, or in other words, some items on the list may be very subjective & based on the background of the writer.
It is very likely that one will wind up using Polymorphic functions to solve a more general problem. And if you define type alias for such a function you may be puzzled what is the right syntax should be used for such type definition.
Let's start with:
/* @flow */
type F = <a> (input:a) => a
const double:F = (x) => x * 2
One (like me) may expect that type of double
will be infered to be bound version of F
: <a:number> = (x:number) => number
but it is not! Flow will report following errors:
src/flowing.js:4
4: const double:F<number> = x => x * 2
^^^^^^^^^ type application of identifier `F`. Expected polymorphic type instead of
4: const double:F<number> = x => x * 2
^^^^^^^^^ type `F`
src/flowing.js:4
4: const double:F<number> = x => x * 2
^^^^^^^^^^ arrow function. Expected polymorphic type instead of
4: const double:F<number> = x => x * 2
^^^^^^^^^ type `F`
Found 2 errors
Issue here (as far as I undrestand it) is that flow does not try to infer or put bounds onto double
instead it verifies that implementation is in fact satisfies claimed type, while in our case it is bound to a very concrete number
type instead of a general a
type.
One may assume that you could anotate double
with a more concerte type of F
- put a bounds onto polymorphic type or more specifically say that a
in this case should be a number
as follows:
/* @flow */
type F = <a> (input:a) => a
const double:F<number> = (x) => x * 2
But that also not something that will type check:
src/flowing.js:4
4: const double:F<number> = (x) => x * 2
^^^^^^^^^ type application of identifier `F`. Expected polymorphic type instead of
4: const double:F<number> = (x) => x * 2
^^^^^^^^^ type `F`
src/flowing.js:4
4: const double:F<number> = (x) => x * 2
^^^^^^^^^^^^ arrow function. Expected polymorphic type instead of
4: const double:F<number> = (x) => x * 2
^^^^^^^^^ type `F`
Found 2 errors
As far as I understad for flow F
is a type that is a "polymorphic function" but it is not a polymorphic type. Luckily there is a way to define a polymorphic type F
that is a function by just moving a =
character:
/* @flow */
type F <a> = (input:a) => a
const double:F<number> = (x) => x * 2
Finally this type checks & behaves as expected! Although make sure to see next section as polymorphic type of a function has it's own quirks!
Sometimes you need to define type for polymophic functions and then define an implementation for it without binding a polymorphic type. Let's consider following example:
/* @flow */
type Update <model> = (state:model) => model
const incX:Update = (model) => 4
incX({x: 3, y: 4})
Ok in above passed state is {x: 3, y: 4}
and then returns 4
so clearly our incX
does not satisfy claimed Update
type since state
type and return value type are clearly different. Well in fact it's not necesarrily true! In fact in this case we claimed Update
is generic over model
and there for it could be number|{x:number}
and for such model
our incX
is a totally valid is why flow won't report any errors.
So in this case what we really want is to keep Update
type polymorphic but possibly little more concerete! Enter bound polymorpism, which allows us to do just that:
/* @flow */
type Model = {x:number}
type Update <model:Model> = (state:model) => model
const incX:Update = (model) => 4
incX({x: 3, y: 4})
Surprise! Surprise! This stil type checks fine! Ok so what happens here is that flow treats type of incX
as (state:any) => any
I'm not entirely sure why, but at least now we know why you should avoid this.
Luckily though we still can define a type to accomodate our needs, although in this case we would have to use not a bound polymorphic type that is a function but rather a type that is polymorphic function:
/* @flow */
type Model = {x:number}
type Update = <model:Model> (state:model) => model
const incX:Update = (model) => 4
incX({x: 3, y: 4})
Which does not type check since incX
returns value that does not satisfy our Model
type.
Here is a pretty trivial function in JS that can be quite a challenge to type annotate, one that I was unsucessful to complete:
const match = rules => (model, action) => rules[action.type](model, action)
I'll leave it as a challenge to a reader to type anotate that function that would type check & error as expected, instead I would introudce a hack that can be used to trick flow into figuring a type anotation for you:
const Match = rules => (model, action) => rules[action.type](model, action)
export const match:typeof(Match) = Match
Here is a little explanation of the hack! Flow requires you to type anotate only exports and infers everything else. In this case we defire actual implementation without type anotations and use flow's typeof to obtain inferred type of the implementation and then we export implementaion with a different name & type annotated with inferred type.
It seems that flow does not yet properly handle union type of union types which may be little hard to spot in a less trivial code than follows:
/* @flow */
export type AB = {type: "A"} | {type: "B"}
export type C = {type: "C"}
export type ABC = AB|C
export const x:ABC = {type: "A"}
Flow will not type check this:
src/flowing.js:7
7: export const x:ABC = {type: "A"}
^^^ object type. This type is incompatible with
5: export type ABC = AB|C
^^ union type
Found 1 error
Although you can work around this by manually unpacknig involved sub-unions types:
export type A = {type: "A"}
export type B = {type: "B"}
export type AB = A|B
export type C = {type: "C"}
export type ABC = A|B|C
export const x:ABC = {type: "A"}
👍