Can't catch overload with exact parameter function signature with new [0.9.5] overload resolution rules

Topics: Language Specification
Dec 18, 2013 at 11:16 AM
In some scenarios we need to transform function (make new one) with some modifications of its signature. For example, in curry we transform (a, b) => c into a => b => c. I've got an issue with such scenario when tried to define signatures for RxJS's Rx.Observable.toAsync in DefinitelyTyped.

So the problem is again with new call overload resolution rules defined in 0.9.5.

I'll try to describe with simple example.
We need to describe signature of function that take a function as parameter and return a new function with replaced return type but with same parameter list. To simplify, let's say, the parameter function can be without parameter or with one optional parameter (in my code I provide overloads for any combination up to 4 parameters):
// function that removes return parameter
interface F {
    <T>(func: () => T): () => void;
    <T1, T>(func: (a?: T1) => T): (a?: T1) => void; 
}
var f: F;

f(() => 1)();   // ok, infered T is number
f((a?: string) => 1)("");   // fail, used first overload
In second usage example, the first overload is chosen by TS, because (a?: string)=>number convertible to () => number.
Ok, let's switch order of the overloads:
interface G {
    <T1, T>(func: (a?: T1) => T): (a?: T1) => void;
    <T>(func: () => T): () => void; 
}
var g: G;

g(() => 1)();   // used first overload, but expected that will be used second
g(() => 1)(1);  // ok, but expected error
g((a?: string) => 1)("");   // ok, used first overload
In this case, for first call with () => number function as argument used first overload that accepts (a?: {}) => number (actually via bug #2031 it infers (a?: number) => {}).

So here is the problem:
  • when we have two functions with signatures A, B
  • A convertible to B and B convertible to A (like ()=>number and (a?:any)=>number)
  • when we need different overloads for parameters of type A and B
    then any ordering of overloads won't solve the task.
To solve it we need overload resolution that tries to choose the best of overloads.
Pleeease, revert previous resolution mechanism. It was much much better. =)
Dec 18, 2013 at 6:00 PM
I believe a similar problem also affects definition files for libraries like BreezeJS. Their use of overloads to describe their using function no longer compiles in 0.9.5 as it did in 0.9.1.1. I suspect this is because the types MergeStrategySymbol and FetchStrategySymbol are convertible.
class EnumSymbol {
  parentEnum: IEnum;
  getName(): string;
  toString(): string;
}

class MergeStrategySymbol extends breezeCore.EnumSymbol {
}

class FetchStrategySymbol extends breezeCore.EnumSymbol {
}

...

class QueryOptions {
  static defaultInstance: QueryOptions;
  fetchStrategy: FetchStrategySymbol;
  mergeStrategy: MergeStrategySymbol;

  constructor (config?: QueryOptionsConfiguration);

  setAsDefault(): void;
  using(config: QueryOptionsConfiguration): QueryOptions;
  using(config: MergeStrategySymbol): QueryOptions; // **** Compiler Error in TS 0.9.5. ****
  using(config: FetchStrategySymbol): QueryOptions;  // **** Convertible to MergeStrategySymbol ****
}

...
For situations like this, what is the recommended fix?
Dec 18, 2013 at 6:24 PM
Yes, it have the same reasons.
More then that, by new specification impossible (compiler error) to define overloads with exactly same signatures.
In your example MergeStrategySymbol and FetchStrategySymbol have exactly same structure, so its same type.
I agree, that it may fix some problems, but I think compiler should only generate warning and allow to define overloads when it use different type reference (names of type). In your code, I think, no errors/warning should be, but warning if you define overload with same names more then once.

Let me show example. Consider we have base.d.ts library:
interface Smth {
  f(): void;
}
interface Other {
  g(): void;
}

var SuperLib: {
  (smth: Smth): Smth;
  (other; Other): Other;
};
This code compiles with no errors.
And we have children libraries, that provide additional definitions to our interfaces Smth and Other (like most of jQuery extensions). base.smth.d.ts:
///<reference path="base.d.ts"/>
interface Smth {
  g(): void;
}
and base.other.d.ts:
///<reference path="base.d.ts"/>
interface Other {
  f(): void;
}
If we'll use just one of the children libraries it will be compiled with no errors.
But if we will use both of them, interfaces Smth and Other become identical by structure and overload in base.d.ts will fail with compiler error.
It's so unexpected!
Dec 18, 2013 at 8:00 PM
Agreed. It certainly unexpected and doesn't seem ideal.

Furthermore, if you are relying on TypeScript to help provide tooling like VS with IntelliSense it's also problematic. In this case when you are trying to define different overloads with varying behaviors (that have different type references, but are structurally the same) it makes it impossible.