Reputation: 26142
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.
Upvotes: 0
Views: 527
Reputation: 26142
@nitzan-tomer
You see, there seem to be a better solution:
interface CurriedFunction {
<T1, T2, R>(t1: T1, t2: T2): R;
<T1>(t1: T1): <T2, R>(t2: T2) => R;
}
interface Assoc {
(prop: string): CurriedFunction
}
There is a good discussion of this topic: https://gist.github.com/donnut/fd56232da58d25ceecf1
Upvotes: 0
Reputation: 164147
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) => "";
}
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'));
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.
Upvotes: 1