Jason Kleban
Jason Kleban

Reputation: 20788

Conditional types ReturnType function signature overload resolution

I'm trying to use typescript 2.8's new conditional types, (not yet released version 2.8.0-dev.20180307) and I don't know if this is a bug or just a misuse. My focus is on my declaration of MockedImplementation<F> which can be 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 to be 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 of SomeOperationA works, perhaps directly because there are no function signature overrides. But mockIt of SomeOperationB fails:

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.

It seems to be intersecting types instead of unioning them? But I'm sure it is more nuanced than that.

I saw a note somewhere about "considers the last overload because presumably it is the most generalized", but I don't think it is relevant here because it doesn't seem to behave as though it matters.

Edit

@jcalz is right, makes sense:

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

Upvotes: 2

Views: 801

Answers (1)

jcalz
jcalz

Reputation: 328433

A smaller reproduction of your problem with no conditional types:

declare function acceptSomeOperationB(x: SomeOperationB): void;
acceptSomeOperationB(() => Promise.resolve('hello')); // ❌ Type 'string' is not assignable to type 'Wrapped<string>'.

It is clear that TypeScript does not consider the arrow function to be compatible with SomeOperationB because it fails to meet one of the overloaded signatures. Indeed, if you pass true as the second parameter to that function, it will not return a Promise<Wrapped<string>>, as required by the second signature for SomeOperationB.

Once you decide how to resolve that, it should start working (or you can at least move on to issues with conditional types.)

Good luck.

Upvotes: 3

Related Questions