Definitelytyped: @types/jasmine: False compile error when methods are overloaded

Created on 21 Mar 2019  路  7Comments  路  Source: DefinitelyTyped/DefinitelyTyped

Since #33860 we get false compile errors when spying on overloaded methods.

Minimal example:

interface I {
  f(): string;
  f(x: any): number;
}

const spyObject = jasmine.createSpyObj<I>("spyObject", ["f"]);
spyObject.f.and.returnValue("a string, erroneously required to be a number");

Results in:

TS2345: Argument of type '"a string, erroneously required to be a number"' is not assignable to parameter of type 'number'.

Our expectation: The accepted parameter type of returnValue should be string | number, not only number.

(Unfortunately the problematic method overloading (as demonstrated in the I example interface) occurs in external libraries we use, so we can't change that part.)

CC PR author: @kolodny

All 7 comments

Hmm, this seems to be a typescript error. I've created a PoC that highlights this:

https://www.typescriptlang.org/play/#src=type%20ReturnObjects%3CT%3E%20%3D%20%7B%0A%20%20%20%20%5BK%20in%20keyof%20T%5D%3A%20T%5BK%5D%20extends%20((...args%3A%20any%5B%5D)%20%3D%3E%20any)%20%3F%0A%20%20%20%20T%5BK%5D%20%26%20Returnable%3CT%5BK%5D%3E%20%3A%0A%20%20%20%20never%0A%7D%0A%0Atype%20Returnable%3CFun%20extends%20(...args%3A%20any%5B%5D)%20%3D%3E%20any%3E%20%3D%20%7B%0A%20%20%20%20returnValue(val%3A%20ReturnType%3CFun%3E)%3A%20void%3B%0A%7D%0A%0Ainterface%20I%20%7B%0A%20%20%20%20f()%3A%20string%3B%0A%20%20%20%20f(x%3A%20any)%3A%20number%3B%0A%7D%0A%0Alet%20argsObj%3A%20ReturnObjects%3CI%3E%3B%0AargsObj.f.returnValue(1)%20%2F%2F%20OK%0AargsObj.f.returnValue('1')%20%2F%2F%20ERROR%3F!%0A%0A%0A%0Atype%20MapStrings%3CT%3E%20%3D%20%7B%0A%20%20%20%20%5BK%20in%20keyof%20T%5D%3A%20T%5BK%5D%20extends%20((...args%3A%20any%5B%5D)%20%3D%3E%20string)%20%3F%0A%20%20%20%20T%5BK%5D%20%26%20Returnable%3CT%5BK%5D%3E%20%3A%0A%20%20%20%20never%0A%7D%0A%0Atype%20MapNumbers%3CT%3E%20%3D%20%7B%0A%20%20%20%20%5BK%20in%20keyof%20T%5D%3A%20T%5BK%5D%20extends%20((...args%3A%20any%5B%5D)%20%3D%3E%20number)%20%3F%0A%20%20%20%20T%5BK%5D%20%26%20Returnable%3CT%5BK%5D%3E%20%3A%0A%20%20%20%20never%0A%7D%0A%0Alet%20tester%3A%20MapStrings%3CI%3E%20%7C%20MapNumbers%3CI%3E%3B%0Atester.f()%20as%20string%3B%20%2F%2F%20OK%0Atester.f(1)%20as%20number%3B%20%2F%2F%20OK%0Atester.f.returnValue(123)%3B%20%2F%2F%20OK%0Atester.f.returnValue('123')%3B%20%2F%2F%20ERROR%3F!%0A%0A%2F%2F%20This%20seems%20to%20work%20when%20manually%20wiring%20up%20the%20union%20types%0A%0Alet%20a1%20%3D%20%7B%20returnValue%3A%20(arg%3A%20string)%20%3D%3E%20%7B%20%7D%20%7D%0Alet%20a2%20%3D%20%7B%20returnValue%3A%20(arg%3A%20number)%20%3D%3E%20%7B%20%7D%20%7D%0Alet%20manualUnion%3A%20typeof%20a1%20%26%20typeof%20a2%3B%0AmanualUnion.returnValue(3)%20%2F%2F%20OK%0AmanualUnion.returnValue('3')%20%2F%2F%20OK%0A%0A%2F%2F%20The%20function%20doesn't%20seem%20to%20be%20the%20cause%20since%20this%20works%20as%20well%0Ainterface%20A%20%7B%0A%20%20%20%20()%3A%20number%0A%20%20%20%20returnValue%3A%20(arg%3A%20number)%20%3D%3E%20void%0A%7D%0A%0Ainterface%20B%20%7B%0A%20%20%20%20(x%3A%20any)%3A%20string%0A%20%20%20%20returnValue%3A%20(arg%3A%20string)%20%3D%3E%20void%0A%7D%0A%0Alet%20ab%3A%20A%20%26%20B%3B%0Aab()%20as%20number%3B%20%2F%2F%20OK%0Aab(%7B%7D)%20as%20string%3B%20%2F%2F%20OK%0Aab.returnValue(1)%20%2F%2F%20OK%0Aab.returnValue('1')%20%2F%2F%20OK

type ReturnObjects<T> = {
    [K in keyof T]: T[K] extends ((...args: any[]) => any) ?
    T[K] & Returnable<T[K]> :
    never
}

type Returnable<Fun extends (...args: any[]) => any> = {
    returnValue(val: ReturnType<Fun>): void;
}

interface I {
    f(): string;
    f(x: any): number;
}

let argsObj: ReturnObjects<I>;
argsObj.f.returnValue(1) // OK
argsObj.f.returnValue('1') // ERROR?!



type MapStrings<T> = {
    [K in keyof T]: T[K] extends ((...args: any[]) => string) ?
    T[K] & Returnable<T[K]> :
    never
}

type MapNumbers<T> = {
    [K in keyof T]: T[K] extends ((...args: any[]) => number) ?
    T[K] & Returnable<T[K]> :
    never
}

let tester: MapStrings<I> | MapNumbers<I>;
tester.f() as string; // OK
tester.f(1) as number; // OK
tester.f.returnValue(123); // OK
tester.f.returnValue('123'); // ERROR?!

// This seems to work when manually wiring up the union types

let a1 = { returnValue: (arg: string) => { } }
let a2 = { returnValue: (arg: number) => { } }
let manualUnion: typeof a1 & typeof a2;
manualUnion.returnValue(3) // OK
manualUnion.returnValue('3') // OK

// The function doesn't seem to be the cause since this works as well
interface A {
    (): number
    returnValue: (arg: number) => void
}

interface B {
    (x: any): string
    returnValue: (arg: string) => void
}

let ab: A & B;
ab() as number; // OK
ab({}) as string; // OK
ab.returnValue(1) // OK
ab.returnValue('1') // OK

I created https://github.com/Microsoft/TypeScript/issues/30545 for this

Looks like it's a design limitation of typescript 馃檨

Has anyone gotten around this?

It doesn't sound like there's much that can be done for this issue since the language isn't capable of processing this case. I would suggest using a form of type erasure to work around this issue:

type AnyMethods<T> = {
  [K in {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
  }[keyof T]]: (...args: any[]) => any
}

interface I {
  f(): string;
  f(x: any): number;
}

const spyObject = jasmine.createSpyObj<AnyMethods<I>>("spyObject", ["f"]);
spyObject.f.and.returnValue("a string - working");

I think it would be good to revert this feature which makes this issue. I believe you had good intentions but it basically makes jasmine types not usable if you have typescript method overrloading in code.

Suggested workaround is not so nice - for now we need to stay with old version but I hope it is temporary.

We also opted to pin to 3.3.11

I suggest to keep the feature - it still helps more than harms.

There is also another workaround - just perform local cast in a problematic place:

spyObject.f.and.returnValue("still can return string" as any);

Given the complexity of types we could have, it's not always possible to correctly infer types, so usage of any in particular places still could happen. Especially given that it's a test project, where we normally have more freedom.

Will make a PR soon introducing even better workaround - if you don't need type check, just cast spy to either NonTypedSpyObj<T> for object spies or Spy for function spies. This way you will be permitted to supply arbitrary args and return value.

Was this page helpful?
0 / 5 - 0 ratings