Multimatch Overloads and a New User Contract for Overload Declarations. #57004
Labels
Awaiting More Feedback
This means we'd like to hear from more people who would be helped by this feature
Suggestion
An idea for TypeScript
π Search Terms
β Viability Checklist
β Suggestion
Note: the terms cover, convex, concave, and gap are defined in the Definitions section at the end of this proposal.
Existing overload matching algorithm - Single Match
The current overload matching algorithm selects a single overload, or fails, as follows:
pseudo-code:
Proposed overload matching algorithm (defective version) - Multiple Matches
First, a defective version of the proposed algorithm is described here. Further down, a better version is described.
The (defective) proposed overload matching algorithm selects (possibly multiple) overloads as follows:
pseudo-code:
What are the consequences of this proposed change?
This table shows relations between the input argument range and compile errors, for the current (Single Match) and proposed (Multiple Matches) overload matching algorithms, without any other changes. This is for the general case where last overload might not be the cover.
*Table of Compiler Errors for Input Argument Range vs Algorithm (with Defective Proposed Algorithm)
First and foremost, the current (Single Match) algorithm is designed specifically so that any input overlapping the (non-empty) Gap of the overload sequence will fail to match any overload, and will trigger a compile error. This is a designed feature, not a bug. The idea is that no unhandled input can slip through resulting in unpredictable behavior. That's the current contract between the user and TypeScript, and it is a good contract for the user.
In contrast, in the proposed matching algorithm described so far, any input overlapping (case 3), or exceeding (case 4), the Gap of the overload sequence might not trigger a compile error. Unless other changes are made, the proposed algorithm will be a bad contract for the user. The next section describes changes to make it a good contract again.
Contract with User to Handle the Gap Case (User Runtime Contract)
Interface and Usage
Suppose an overload function declaration
An new intrinsic function is proposed:
where
functionSymbol
is the symbol for the overload function declaration,gapReturnType
is a type that is eitherthrows
,never
, or any other type,throws
a keyword but not a new type. To be specific:throws
is tonever
asvoid
is toundefined
.thrownType
indicated the type that should be thrown whengapReturnType
isthrows
, otherwise ignored.Note: The presence of
throws [optional type]
doesn't imply that full throw flow support is required. It is sufficient to display it as a return type as a form of documentation.Note that
SetupOverload
does not fit the TypeScript syntax of a generic type function, becausefunctionSymbol
is not a type.functionSymbol
is necessary to associate the type with the function symbol.typeof overloadFunction
.Creating an overload type independently of a function declaration is described later.
The function
SetupOverload
will set up an overload type assiciate with the symbol foroverloadFunction
.The overload type will provide the following public interface for TypeScript internal use only:
getParametersCover()
:explicitOverloads
provided inSetupOverload
Parameters<typeof overloadFunction>
getReturnCover()
:explicitOverloads
provided inSetupOverload
ReturnType<typeof overloadFunction>
getExplcitOverloads()
:SetupOverload
overloadFunction
implimentation.getGapReturnType()
:SetupOverload
overloadFunction
implimentation.getThrownType()
:SetupOverload
overloadFunction
implimentation.SetupOverload
needs to be executed by the compiler before the implementation ofoverloadFunction
undergoes flow analysis / type checking, where it will used to check return types (as much as possible).The valid values for
CoverReturnType
and their meanings are as follows:throws
: This is a new keyword. When the user specifiesthrows
, the user is agreeing to handle the Gap case in runtime by throwing an exception.never
: When the user specifiesnever
, they are agreeing to one of two things. Either:The value
gapReturnType
should be displayed as part of the overload type, so that clients of the user can see the user's contract.Note about
throws
throws
is still a meaningful contract for both the user, and clients of the user. I.e., implementation of functionthrows
flow in flow analysis is not a prerequisite to usingthrows
as aCoverReturnType
value. In fact, thethrows
keyword can also be used as a return value for any explicitly defined overload, for the same reason.Overload matching algorithm - Multiple Matches (non-defective version)
The full, non-defective, proposed Multi Match algorithm becomes as follows:
pseudo-code:
The updated Table of Compiler Errors becomes:
Table of Compiler Errors for Input Argument Range vs Algorithm
The difference between the cases is now narrowed to case 3.
The use of "User Runtime Contract" is a tradeoff, which will be examined in the Examples section below.
Implicit final overload
Conceptually, the overload type has an implicit final overload in addition to the explicit overloads. That implicit overload has implicit parameter type of the Gap of the overload sequence parameters, because the Gap is all that is left after matching the explicit overloads.
The return type of the implicit overload is
gapReturnType
.The parameters of the implicit overload are the Cover of the parameters of the explicit overloads. However the return type is only the
gapReturnType
.Creating an Overload Type without a Function Declaration
It is also necessary to be able to create an overload without reference to a function declaration.
This could be done with the following intrinsic function:
π Motivating Example
Example 1: Overloads in a nested loop
Based on case study borrowed and expanded from @RyanCavanaugh.
TypeScript 5.3.2
Under this proposal -
Proposal
To be fair, TypeScript 5.3.2 overloads could be written to capture all the possible valid inputs, but it requires considering all these cases:
string
,number
,string|number
} X {boolean
,number
,boolean|number
} X {string
,boolean
,string|boolean
}which is a total of 27 cases, and figuing out the return type for each case.
So it's not a good solution. Not to mention, even after removing the returns with value never, the number of matches for the compiler to perform is prohibitive.
Another 5.3.2 solution is to recreate the implementation inside the loop, which is redundant and error prone:
so that's not a good solution either.
Example 2: Concave Overload Sequence
Borrowed and extends from TypeScript documentation on Overloads:
With just the two overloads it is a Concave Overload Sequence.
TypeScript 5.3.2
so we add two more overloads to make it a Convex Overload Sequence.
TypeScript 5.3.2
The proposal allows a more simple declaration:
Example 3: Capturing input correlation with return type
For some overload functions with simple return types, the reverse function mapping allows a flow-analysis functionality similar to that
that
const
variables has with respect to flow analysis - they can capture the correlation between multiple variables.π» Use Cases
As in example 1, there are some cases where single-exact-match overloads cannot effectively capture the range of valid combinations of types. Partial matching solves that.
In many cases providing the gap overload parameters and final cover are tedious because that require accurately writing a complicated type, which is why it is often skipped, as in example 2. The advantages of using
setupOverload
areconst
variables has with respect to flow analysis - they can capture the correlation between multiple variables. This may be useful in some cases.More details required
Implementing the new overload type so that is coexists with old overload type
So that it wouldn't be a breaking change.
Inference, unions, and intersections of the proposed overloads
There are implications for inference, unions, and intersections of the proposed overloads which need to be detailed.
Definitions
Definition of Cover of Overloads
Suppose a sequence of overloads for a function
f
is defined as follows:The cover of the sequence of overloads is a unique type,
and that type could be defined canonically as follows:
Notice the cover is formed by taking the union of types over each parameter (and return type).
psuedo-code:
Definition of Convex vs Concave Overloads, the Gap.
"Concave" and "convex" are terms borrowed from sets theory and geometry, where here the parameters and return type are the spaces axes.
"Gap", on the other hand, is a shorthand defined in this explanation only.
Here is an example of a convex sequence of overloads (borrowed from the TypeScript documentation):
The type of the cover of the above sequence of overloads is unique. It could be defined as follows:
which is equivalent to the canonical definition above:
The input
...[1,1]
is in the gap of the above sequence of overloads.A more general example
In both case, the TypeScript compiler kindly informs us that the input overlaps the gap of the sequence of overloads.
The text was updated successfully, but these errors were encountered: