Exploring the usage of TypeScript 2.8's upcoming conditional types, I encountered an issue with my declaration of MockedImplementation<F>
. This declaration is intended to encompass a full function matching F
, the return type of F
, and if the return type of F
is a Promise
, then it could also be what the promise resolves to - all wrapped accordingly by mockIt()
.
type MockedImplementation<F> =
F | // The function signature
((ReturnType<F extends (...args: any[]) => any ? F : any>) extends infer T
? T extends Promise<infer R>
? (T | R) // Or the promise or just the type that the promise resolves to
: T // or whatever type this is
: never);
interface Wrapped<T> {
result: T
}
function mockIt<F>(pretend : MockedImplementation<F>) : F {
throw new Error('Not Implemented'); // doesn't matter
}
interface SomeOperationA {
(parameters : { 'a': number[], 'b'?: string }) : Promise<string>;
}
mockIt<SomeOperationA>(() => Promise.resolve('hello')); // 👍 OK
mockIt<SomeOperationA>(Promise.resolve('hello')); // 👍 OK
mockIt<SomeOperationA>('hello'); // 👍 OK
mockIt<SomeOperationA>(42); // 👍 Type error.
mockIt
works as expected for SomeOperationA
, potentially due to no function signature overrides. However, when testing with SomeOperationB
, issues arise:
interface SomeOperationB {
(parameters : { 'a': number[], 'b'?: string }) : Promise<string>;
(parameters : { 'a': number[], 'b'?: string }, rawResponse : true) : Promise<Wrapped<string>>;
(parameters : { 'a': number[], 'b'?: string }, rawResponse : false) : Promise<string>;
}
mockIt<SomeOperationB>(() => Promise.resolve('hello')); // ❌ Type 'string' is not assignable to type 'Wrapped<string>'.
mockIt<SomeOperationB>(Promise.resolve('hello')); // 👍 OK
mockIt<SomeOperationB>('hello'); // 👍 OK
mockIt<SomeOperationB>(42); // 👍 Type error.
The behavior seems to involve intersecting types rather than uniting them. It appears more complex than anticipated.
I came across a note suggesting that the system "considers the last overload because presumably it is the most generalized," but in this scenario, it does not seem to have a significant impact.
Edit
@jcalz's perspective sheds new light on the issue:
interface SomeOperationB {
(wrapped : true) : Promise<Wrapped<string>>;
(wrapped : false) : Promise<string>;
}
interface Wrapped<T> { result: T }
declare function acceptSomeOperationB(x: SomeOperationB): void;
acceptSomeOperationB(() => Promise.resolve('hello')); // ❌ Type 'string' is not assignable to type 'Wrapped<string>'.
acceptSomeOperationB(() => Promise.resolve({ result: 'hello' })); // ❌ Type '{ result: string; }' is not assignable to type 'string'.