Writing Flowtype annotations for Ramda: Polymorphism and R.any

Writing more generic types with polymorphism

We’re finally going to put add and subtract to rest, but transition into a discussion on polymorphism by defining NumericFn2 type in terms of the more general type CurriedFn2:

declare type CurriedFn2<T1, T2, R> =
& ((t1: T1, t2: T2) => R)
& ((t1: T1, ...rest: Array<void>) => (t2: T2) => R)

declare type NumericFn2 = CurriedFn2<number, number, number>

this is how the library definition in flow-typed has it defined.

This technique uses polymorphism to define a type which, when provided with other types T1, T2, and R produces a concrete type. For example, if we plug in number in every slot of CurriedFn2, we get the exact definition we had before:

declare type CurriedFn2WithNumber<number, number, number> =
& ((t1: number, t2: number) => number)
& ((t1: number, ...rest: Array<void>) => (t2: number) => number)

Defining a type for R.any

The Ramda documentation for any describes it as a function that “Returns true if both arguments are true; false otherwise.” However, it gives the type signature as * → * → *, instead of the more restrictive boolean → boolean → boolean.

Indeed, you can put any values that have an interpretation as truth or falsy in javascript, and the result of and(x, y) is either the last truthy value, or the first falsy value:

and(true, false); // => false
and(0, 'a'); // => 0
and('', false); // => ''
and([], 0); // => 0
and(() => {}, 'function'); // => 'function'

Arguably, this is not a very good use for and, but it is valid and hinted at by that type signature!

and is polymorphic, but in a “bounded” sense. Most likely, the implementation of and is based on the double-equals (==) operator in JavaScript and will only work with values compatible with the operator. In Haskell, we might define something like and in terms of the Eq typeclass.

So it turns out that doing this sort of works, but might not be expresssive enough:

declare var and: CurriedFn2<any, any, any>

because it wouldn’t catch something like

and(1, 'a') == [];

Which would not cause a runtime error, but is the kind of thing Flow tries to catch by default.

If the first argument of and is T and second is U, then the result is either T or U, and can’t be something random. Thus, a better type for the 2-ary version of and would be (x: T, y: U) => T | U.

declare function and<T1, T2>(x: T1, y: T2, ...rest: Array<void>): T1 | T2;

Then we get the error we expect:

and(1, 'a') == [];
// 12: a == [];
// ^ number. Cannot be compared to
// 12: a == [];
// ^ empty array literal
// 12: a == [];
// ^ string. Cannot be compared to
// 12: a == [];
// ^ empty array literal

To get the unary version, add a separate declaration:

declare function and<T1, T2>(x: T1, ...rest: Array<void>): (y: T2) => T1 | T2;
declare function and<T1, T2>(x: T1, y: T2, ...rest: Array<void>): T1 | T2;

It seems that the order matters. If you put the 2-ary definition before the unary, Flow will throw an error on something like and(1)(1) complaining about not having enough arguments.

So we know that the return type of and(1, ‘a’) is string | number, which means this code will typecheck correctly:

and(1, 'a') == []; // error
and(1, 'a') + '!' // no error
and(1)('a') + '!' // no error
any("ada")([0,0]).concat("A") // no error

Although, weirdly, this does not work!

any(["a", "a"])([0,0]).concat("A")
// 19: any(["a", "a"])([0,0]).concat("A")
// ^ string. This type is incompatible with
// 19: any(["a", "a"])([0,0]).concat("A")
// ^ number
// 19: any(["a", "a"])([0,0]).concat("A")
// ^ number. This type is incompatible with
// 19: any(["a", "a"])([0,0]).concat("A")
// ^ string

This might be an issue specific to unions of tuples. There are some issues on this on Flow Github repository.

What’s Next?

In the next post, we’ll start looking at functions for lists (e.g. map, filter) in Ramda.