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
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.