Writing Flowtype annotations for Ramda: Add and Subtract, Continued

Specifying the exact number of arguments

At the end of the last post, this is how we had defined add and subtract:

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

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

We had some simple tests that seemed to pass:

add(1);
add(1)(1);
add(1)(1) == 'a'; // error

But if we try this:

subtract(1, 'a'); // no error

we don’t get an error! What might be going on? Check this out:

subtract(1, 'a')(1) == 1; // no errors
subtract(1, 'a')(1); // error: number cannot be compared to string

So it seems that subtract(1, ‘a’) is assumed to match the same case as subtract(1), since where the ‘a’ is just assumed to be a superfluous extra argument.

Thus, we need to encode that subtract(1, ‘a’) is not a valid usage by making it explicit that the unary add only takes one argument.

This doc describes the trick for declaring a function with a fixed number of argument. And here is how fix NumericFn2:

declare type NumericFn2 =
& ((x: number, y: number) => number)
& ((x: number, ...rest: Array<void>) => ((y: number) => number));

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

Now this once valid expression:

subtract(1, 'a')(1);

Results in a several Flow warnings explaining why neither type in the intersection will work with it:

If we wanted to, we could do the same trick for the 2-ary version of add, even though something like add(1, 2, 3, 4, 5) is harmless. Having Flow detect something like this would eliminate a code smell, so there might be some value in doing it:

declare type NumericFn1 = (x: number, ...rest: Array<void>) => number;
declare type NumericFn2 =
& ((x: number, y: number, ...rest: Array<void>) => number)
& ((x: number, ...rest: Array<void>) => NumericFn1);

Here, I aliased NumericFn1 to the unary function for conciseness. We could actually use this type for R.inc and R.dec.

Using interfaces

In the TypeScript definitions for Ramda, curried functions are actually defined using an interface:

// https://github.com/donnut/typescript-ramda/blob/master/ramda.d.ts#L75
interface CurriedFunction2<T1, T2, R> {
(t1: T1): (t2: T2) => R;
(t1: T1, t2: T2): R;
}

Here, this definition uses polymorphism, in letting types T1, etc. be variables, while we are using the concrete type number.

The more familiar use case for interfaces comes from OOP, where you might want a class to extend an interface, thus forcing it to have a common API with other classes that extend it.

Even in a language like Scala, a callable object Foo would need to define a method apply, whereby Foo(x) is the same as Foo.apply(x). So the syntax here is kind of unusual to me.

But it turns out that Flow actually supports this too, although this use case is not documented. So if we wanted to, we could do this:

declare type NumericFn1 = (x: number, ...rest: Array<void>) => number;

declare interface NumericFn2 {
(x: number, y: number, ...rest: Array<void>): number;
(x: number, ...rest: Array<void>): NumericFn1;
}

which is really interesting to me. From what I can gather, the errors caused by subtract(1, ‘a’) look the same as when we used an intersection type, but because this is not documented, who knows if this technique is seen as a “good idea” or whether it will continue to be supported.

Next time

Believe or not, we are still not quite done with simple math functions in Ramda! In the next post, I will cover polymorphism and show how it can be used to lay the foundation for non-math functions.

We will also tackle the interesting function and whose type signature is any any any (but not really!) and see how we can safely type such a crazy function.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.