Skip to content

Return types of intersection of functions are incomplete and depend on order of declaration - an algorithm to fix it.Β #57095

Open
@craigphicks

Description

@craigphicks

πŸ” Search Terms

#57002
#41874
intersection of functions, return type, incomplete, declaration order dependent

βœ… Viability Checklist

⭐ Suggestion

The return type of an intersection of functions should be at least complete and independent of the order of declaration of the functions.

πŸ“ƒ Motivating Example

Example 1: intersection of non-overload functions

interface A0 {
    foo(): string;
}

interface B0 {
    foo(): number;
}

declare const ab0: A0 & B0;
declare const ba0: A0 & B0;

const rab = ab0.foo(); // actual string, expecting string | number
const rba = ba0.foo(); // actual number, expecting string | number

Example 2: intersection of overload functions

interface A1 {
    bar(x:1): 1;
    bar(x:2): 2;
}
interface B1 {
    bar(x:2): "2";
    bar(x:3): "3";
}

declare const ab1: A1 & B1;

const rab11 = ab1.bar(1); // actual 1, expecting 1
const rab12 = ab1.bar(2); // actual 2, expecting 2 | "2"
const rab13 = ab1.bar(3); // actual 3, expecting 3

declare const ba1: B1 & A1;

const rba11 = ba1.bar(1); // actual 1, expecting 1
const rba12 = ba1.bar(2); // actual "2:, expecting 2 | "2"
const rba13 = ba1.bar(3); // actual 3, expecting 3

Example 3: intersection of functions with object return types

interface A2 {
    bar(): {a: string};
}
interface B2 {
    bar(): {b: string};
}

declare const ab2: A2 & B2;
declare const ba2: B2 & A2;

const rab21 = ab2.bar(); // actual: {a: string}, expecting: {a: string} | {b: string}
const rba21 = ba2.bar(); // actual: {b: string}, expecting: {a: string} | {b: string}

Current algorithm:

  • Treat g = g[0] & g[1] & ... as though it were an ordered overload function { g[0] ; g[1] ; ....}.
  • Let args be the arguments.
  • return ReturnType<chooseOverload(g, args)>

The current algorithm treats g exactly as though it were an ordered overload function { g[0] ; g[1] ; ....}. Therefore args can match at most one intersection member g[i], resulting in an incomplete and declaration order dependent return type.

Proposed algorithm # 1:

  • Let g = g[0] & g[1] & ... be the intersection of functions.
  • Let args be the arguments.
  • returnType = never // initialize
  • for g[i] in g
    • if g[i] is an overload function
      • returnType = returnType | ReturnType<chooseOverload(g[i], args)>
    • else if args extends Parameters<g[i]> then
      • returnType = returnType | ReturnType<g[i]>
  • return returnType

This is expected to work for the above examples.

Proposed algorithm # 2:

Compared to the current algorithm, Proposed algorithm # 1 is a more expensive computation, but it is also complete and is not dependent on declaration order. If that computation is too expensive, then a simpler algorithm could be used:

  • The return type is union over i of ReturnType<g[i]>. If g[i] is an overload function, it is calculated as the return type of the catch-all (a.k.a. cover) case for that overload sequence.

Justification:

  • Let cover(g) be the catch-all case (a.k.a. the cover) of g. i.e. for each parameter index paramIndex, Parameters<cover(g)>[paramIndex] is the union over i of Parameters<g[i]>[paramIdex], and ReturnType<cover(g)> is the union over i of ReturnType<g[i]>. cover(g) is the smallest upper bound of g that can be represented with a single non-overload function.

In the cases of examples 1 and 3, the answer wouldn't change, proposal # 2 is gives identical return type to that calculated with proposal # 1.

πŸ’» Use Cases

Getting accurate return types from intersections of functions.

What workarounds are you using in the meantime?

When A and B are instances of a generic function, e.g.

type G<T> = { bar: <T>() => T };
type A = G<{a:string}>;
type B = G<{b:string}>;

then a workaround is to use the type

type AB = G<{a:string, b:string}>;

even though the type AB is wider than the intersection of A and B.

That workaround is similar in nature to using Proposed Algorithm # 2.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions