javascripttypescriptfunctionreturn-typetype-mismatch

Typescript return type of intersected functions is mismatched with the inferred type


As I was trying to extract the return type of a request that is intersected, I came across the this mismatch of the return type and the inferred type. Here is the shortened url https://tsplay.dev/mAxZZN

export {}
type Foo =  (() => Promise<string>) & (() => Promise<any>) ;


type FooResult = Foo extends () => Promise<infer T> ? T : null
//   ^?

const a:Foo = async () => {
        return "";
    }

const b = await a();
//    ^?


type Foo2 =  (() => Promise<any>) & (() => Promise<string>);

type FooResult2 = Foo2 extends () => Promise<infer T> ? T : null
//   ^?

const c:Foo2 = async () => {
    return "";
}

const d = await c();
//    ^?

In these 2 above examples the results are mismatched to FooResult:any b:string and FooResult2:string d:any

To make my example more clear instead of having a Foo type I just have a intersected type like HTTPRequest & {json: () => Promise<Type>} to have a correct return type of the request json object.

Is there anyway I can make these 2 be matched correctly to same type? If so how? Thanks for the help in advance! <3


Solution

  • An intersection of function types is equivalent to an overloaded function type with multiple call signatures. And there are known limitations when dealing with such types at the type level. Unless you're trying to use overloads and have a strong need for them, you should consider refactoring to use a single call signature instead.


    When you call an overloaded function, the call is resolved with "the most appropriate" call signature; often the first one in the ordered list that applies:

    // call signatures
    function foo(x: string): number;
    function foo(x: number): string;
    
    // implementation
    function foo(x: string | number) {
        return typeof x === "string" ? x.length : x.toFixed(1)
    }
    
    const n = foo("abc"); // resolves to first call signature
    // const n: number
    
    const s = foo(123); // resolves to second call signature
    // const s: string
    

    So the return type will depend on the input type.


    On the other hand, when you try to infer from an overloaded function type, the compiler pretty much only infers from the last call signature:

    type FooRet = ReturnType<typeof foo>
    // type FooRet = string
    // ^^^^^^^^^^^^^^^^^^^^ not (string & number) or [string, number]
    

    This is mentioned in the Handbook documentation for using infer in conditional types and is considered a design limitation of TypeScript, as mentioned in microsoft/TypeScript#43301.

    There are some possible workarounds to try to tease apart multiple call signature information using conditional types, but they are fragile, and before you even think of using them, you should re-examine your use case.


    If you've got two call signatures with the same parameter types, then they really will not behave well at all:

    function bar(): { a: string };
    function bar(): { b: number }; // why?
    function bar() {
        return { a: "", b: 1 }
    }
    

    When you call the function you'll get the first return type:

    const a = bar();
    // const a: { a: string; }
    

    But when you infer you'll get the last return type:

    type BarRet = ReturnType<typeof bar>;
    // type BarRet = { b: number; }
    

    And there doesn't seem to be a reason to have multiple call signatures in such cases. If you want to get an intersection of return types, you should just have one call signature that does that:

    function baz(): { a: string } & { b: number } {
        return { a: "", b: 1 }
    }
    const ab = baz();
    // const ab: { a: string; } & { b: number; }
    type BazRet = ReturnType<typeof baz>;
    // type BazRet: { a: string; } & { b: number; }
    

    So in your case, (() => Promise<string>) & (() => Promise<any>) is equivalent to an overloaded function whose first no-arg call signature returns Promise<string> and whose second no-arg call signature returns Promise<any>. So you'll get the first when you call and the last when you infer. Instead you should just have a single call signature like () => Promise<string> or () => Promise<any> or whatever your desired type is (the any type is problematic itself, but I won't digress further here to talk about it).

    Playground link to code