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
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]
ing
- if
g[i]
is an overload functionreturnType = returnType | ReturnType<chooseOverload(g[i], args)>
- else if
args
extendsParameters<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
i
ofReturnType<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 overi
ofParameters<g[i]>[paramIdex]
, andReturnType<cover(g)>
is the union overi
ofReturnType<g[i]>
.cover(g)
is the smallest upper bound ofg
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.