Skip to content

Instantly share code, notes, and snippets.

@Gozala
Last active March 19, 2020 15:09
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gozala/166b5dc9d453481d0bec to your computer and use it in GitHub Desktop.
Save Gozala/166b5dc9d453481d0bec to your computer and use it in GitHub Desktop.
WTF Flow ?

WTF Flow

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.

Polymorphic type that is a function

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!

Related issues:

Polymorphic function type

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.

Related issues:

When you can't type just trick flow into infering type for you

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.

Related issues

Union of unions

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"}
Related issues:
@syzer
Copy link

syzer commented Oct 5, 2017

👍

@AlexanderShushunov
Copy link

@LoganBarnett
Copy link

Looks like your union of unions section no longer applies, and type checks properly:

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBTAHgBzgJwBcxCBPHDMAQQCEwBeMAbzIoC4wAiKzgXzAA+zVhg6cafTLgLERYAMINh5UV3mTseIiRXUaixrQHz0mmWADGcAHYBnYlja0Dy9lx68gA

I didn't follow back to see when it started passing. facebook/flow#582 is also reported as closed/fixed now.

@LoganBarnett
Copy link

Also I came across this wondering why I was seeing issues with polymorphic types and functions. I'm not sure I fully understand the fix yet, but it helped a lot. Thanks!

@misha-erm
Copy link

You saved my day

was declaring my type as type F = <a> (input:a) => a and had no thoughts that I've put generic in to a wrong place.
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment