Writing Flowtype annotations for Ramda: Add and Subtract

Function types

Ramda provides simple mathematical functions like add and subtract, which do what you might expect, but are (like all functions in Ramda) curried:

// number -> number -> number
add
// number
const two = add(1, 1);
// number -> number
const addOne = add(1);
// number
const three = addOne(2);

This means, in JavaScript, these functions are overloaded, since function calls with different numbers of arguments have different return values.

Thus, add needs two function declarations to account for both kinds of calls:

// types.js
declare function add(x: number, y: number): number;
declare function add(x: number) : (y: number) => number;

// test.js
add(1);
add(1)(1);
add('a'); // error
add(1)(1) == 'a' // error

The second declaration uses Flow’s syntax for typing anonymous functions, which sort of looks like ES2015 arrow function syntax. (y: a) => b is the type for a function which takes one argument of type a, and returns a value of type b. In Haskell syntax, this is the same as a -> b. The argument name y can be anything; it doesn’t need to match the name in the implementation.

DRYing up with function types

add and subtract have the same type, so while this works:

declare function add(x: number, y: number): number;
declare function add(x: number): (y: number) => number;
declare function subtract(x: number, y: number): number;
declare function subtract(x: number): (y: number) => number;

we can express the sameness of these functions by combining them into a single type:

// note: the leading `&`s are a stylistic choice
declare type NumericFn2 =
& ((x: number, y: number) => number)
& ((x: number) => ((y: number) => number))

declare var add: NumericFn2;
declare var subtract: NumericFn2;

This defines add and subtract as an intersection type, meaning that these functions simultaneously satisfy both types in that expression.

Digression: Why not a union type?

Based on how add is used, it might seem more sensible to model it as a union type: as either a 2-ary or unary function. Personally, I am tempted towards this interpretation, since union in set theory is analogous to disjunction in logic. This , however, is actually somewhat misleading. Hence, this digression to try and clear things up.

Suppose we were to model NumericFn2 as a union type, like so:

declare type NumericFn2 =
| ((x: number, y: number) => number)
| ((x: number) => ((y: number) => number))

then you will see this error:

add(1)
^^^^^^ undefined (too few arguments, expected default/rest parameters). This type is incompatible with
6: | ((x: number, y: number) => number)
^^^^^^ number. See lib: flow-typed/types.js:6

It turns out that when you give a value a union type, then you are assuming that the value will be compatible with every possibility encoded in that type, especially if you are not doing any dynamic tests in your code to “refine” the value’s type.

The expression add(1) is incompatible with the first type in the union, because y is a required variable. But even if we were to make that an optional variable:

declare type NumericFn2 =
| ((x: number, y?: number) => number)
| ((x: number) => ((y: number) => number))

This is not only incorrect based on how add actually works, but now this is a problem:

add(1)(1)
// error: Function cannot be called on number

The first 2-ary function type is still problematic, but this time because the return value is assumed to be a number.

So basically, while union types are easier to tag variables with (since they are more generous), such variables are harder to actually do anything with. Without refinement or dynamic type checks, there are fewer guarantees on what you can do with them. For example, consider this code:

type Flexible = string | number;
function foo(x: Flexible) {
const withBang = x + "!"; // okay, since `+` works with both
const times2 = x * 2; // not okay, since you can't multiply strings
}

If we were to make Flexible even more flexible as string | number | Object, then nothing in the function would type check.

On the other hand, while intersection types are more restrictive, this also means there are more things you can safely do with it.

Next time

Amazingly, we are not quite done with add and subtract, even though we have already covered a lot of concepts. In the next post, I will try to stress test these declarations to make sure that they are as safe and useful as they can be. I will also introduce other techniques for DRYing up these types.