WHITECOLOR WHITECOLOR - 27 days ago 6
TypeScript Question

Type function that returns curried function

Say we have assoc function like ramda.assoc (http://ramdajs.com/docs/#assoc)

This function is for setting property on the object. So we want to make typings for function variant with one property name argument .

That function takes one argument property name and returns a curried function which may take one or two arguments:


  • if it takes two arguments (value and object) it sets value on property on the
    object and returns modified object with property set to value.

  • if it takes one argument (value) it returns a function that takes object to modify.



Is it possible to make some typing so TS could handle both cases? This is my take, but it doesn't work:

interface Assoc {
// we may describe both signatures but only first will work
(prop: string): <T, U>(val: T, obj: U) => U;
(prop: string): <T>(val: T) => <U>(obj: U) => U;
}

// implementation
const assoc: Assoc = (prop: string): any => {
....
}

// then there is two function for testing both cases

// this one expects function with one arg that returns function
const map = (func: (some: string) => (x: number) => 1) => {
}

// this one expects function with two args that returns value
const map2 = (func: (some: string, other: string) => 1) => {

}

map(assoc('xxx')) // gives an error

map2(assoc('xxx')) //works ok


I want both to work.

Answer

I don't think that it's possible.
Both signatures expect the same parameter and so the compiler has no way of inferring which of the signature to take, taking the first one is probably the default behavior.

This makes perfect sense, as the compiler is trying to look after you and error when he thinks that you might be making a mistake.
In this case the compiler doesn't have enough information to know whether you're are right or not, because assoc can return different types based solely on the input which is identical in both cases.

If you'll try to write an implementation for such a method in typescript the compiler won't let you:

function fn(base: number): (a: number) => number;
function fn(base: number): (a: number, b: number) => string;
function fn(base: number) { // Error: No best common type exists among return expressions
    if (base < 10) {
        return (a: number) => base * a;
    }

    return (a: number, b: number) => "";
}

(code in playground)

But if you are using a js library that is doing that, then you can tell the compiler that you know what you are doing by asserting it:

type Assoc1 = (prop: string) => <T, U>(val: T, obj: U) => U;
type Assoc2 = (prop: string) => <T>(val: T) => <U>(obj: U) => U;
type Assoc = Assoc1 | Assoc2;

const assoc: Assoc = (prop: string) => {
    return null;
}

const map = (func: (some: string) => (x: number) => number) => {} 

const map2 = (func: (some: string, other: string) => string) => {}

map((assoc as Assoc2)('xxx'));
map2((assoc as Assoc1)('xxx'));

(code in playground)

It's not as elegant, but I'm pretty sure that you have no way around it.
If anyone can come up with a way, I'd love to see it.

Comments