Generic resolution problems

Topics: General, Language Specification
Dec 17, 2013 at 10:34 AM
Edited Dec 17, 2013 at 11:00 AM
in the following example example, there is 2 little things that I do not understand:
  • first without the second overload of autoCurry definitions the compiler won't accept function that returns void, while this is not really problematic since there is workaround it does not feel very natural.
  • My main concern is about the call of myArrayEach2 in this case value has type any :/.
interface Curried2<L, M, N> {
    (l: L, m: M): N;
    (l: L): (m: M) => N;
}

declare var autoCurry: {
   
    <L, M, N>(func: (l: L, m: M) => N ): Curried2<L, M, N>;
    
    <L, M>(func: (l: L, m: M) => void ): Curried2<L, M, void>;
}
    
var myArray: string[] = ["hello","world"];

    
var eachString = autoCurry(function (array: string[], callback: (value: string) => void){
    array.forEach(callback);
}) 


var myArrayEach = eachString(myArray);  
myArrayEach(value => alert(value.toLowerCase()));


var each = autoCurry(function <T>(array: T[], callback: (value: T) => void){
    array.forEach(callback);
}) 

    
var myArrayEach2 = each(myArray);
myArrayEach2(value => alert(value.toLowerCase()));

Is there any way to define what I want without losing typing ?
Developer
Dec 17, 2013 at 11:40 PM
Few issues here:

Fundamentally what you're trying to do with each + autoCurry isn't going to work this way. The compiler can't unify the generic signature passed to autoCurry and autoCurry's generic parameters and leave you with a new generic signature type (each) that will be instantiated later based on the provided arguments. What you can do instead is something like this:
function makeCurriedEach<T>(x: T[]) {
    var each = autoCurry(function (array: T[], callback: (value: T) => void) {
        array.forEach(callback);
    })

    return each(x);
}

var myArrayEach2 = makeCurriedEach(myArray);
myArrayEach2(value => alert(value.toLowerCase())); // value correctly typed as string

var myArrayEach3 = makeCurriedEach([1,2,3]);
myArrayEach3(value => alert(value.toFixed())); // value correctly typed as number
Now the function passed to autoCurry is not generic (ie does not introduce its own type parameters) and things can work how you want.

In addition, note a couple things:
1) There is currently a bug in what the compiler does with your original autoCurry call that takes a generic function. We're in the process of fixing it now, but it won't actually make your pattern work anyway.
2) Type inference will choose {} (empty object) for a type parameter for which no inferences can be made. When you see this type in error messages or Quick Info of generics it's usually a sign something is not working as you expected. In your original code you'll note that the type of each has a few such types in it. This is the clue that type inference for autoCurry did not succeed in the way you expect. The reason you get an error when autoCurry doesn't have the second overload is because N is inferred to be {}, but the function you provided returns void, and void cannot be assigned to {}. The second overload allows this call to work, although L and M are still inferred to be {} themselves. This is what also causes the issue you're seeing with value. The type of each is (m: {}) => void. So myArrayEach2's parameter type is just {}, which permits you to pass a function as an argument, but since the parameter is not a function type there's no contextual typing to apply to 'value.'

Does that make sense?
Dec 18, 2013 at 8:23 AM
Very clear explanation thanks.
However there are some little things that I feel that they could be improved in the language concerning generics :

If I want to describe each without autoCurry I'll do that :
declare function each<T>(target: T[], callback: (t: T) => void): void;
declare function each<T>(target: T[]): (callback: (t: T) => void) => void; 
But when I look at my Curried2 interface :
interface Curried2<L, M, N> {
    (l: L, m: M): N;
    (l: L): (m: M) => N;
}
each definition is in fact Curried2<T[], (t: T) => void, void>

In general there is no way to define a function as a 'specialized' generic function of another generic function signature, even if, in a certain way, we can do that for class :
class Base<A, B> {
    a: A;
    b: B;
}

declare Specified<T> extends Base<T[], (t: T) => void> {
    
}
While it's not a very important feature, I think it could be a nice addition to the generic system.
Anyway thanks again for your explanation.