microsoft / microsoft/TypeScript

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

Open
#57,095 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Awaiting More Feedback Suggestion
Dominant language
Go
Stars
111k
Forks
14.4k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the issue's three intersection-of-functions examples and comparing the two proposed algorithms with the current ordered-overload behavior. The work is done when return types are complete and independent of intersection declaration order, including overloaded functions and object return types.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.