microsoft / microsoft/TypeScript
Return types of intersection of functions are incomplete and depend on order of declaration - an algorithm to fix it.
Nobody has claimed this yet.
- 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
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ 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
argsbe 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
argsbe the arguments. returnType = never// initialize- for
g[i]ing- if
g[i]is an overload functionreturnType = returnType | ReturnType<chooseOverload(g[i], args)>
- else if
argsextendsParameters<g[i]>thenreturnType = returnType | ReturnType<g[i]>
- if
- 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
iofReturnType<g[i]>. Ifg[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) ofg. i.e. for each parameter indexparamIndex,Parameters<cover(g)>[paramIndex]is the union overiofParameters<g[i]>[paramIdex], andReturnType<cover(g)>is the union overiofReturnType<g[i]>.cover(g)is the smallest upper bound ofgthat 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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