-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compiler option to require final "catch-all" case in overload declarations #57057
Comments
So one issue with this I can see is: even if it’s opt-in, basically all library writers would have to adopt it, even if it doesn’t accurately model their API, because there’s no telling whether an end-user will have it enabled for their project. There’s no concept of per-module compiler options in TypeScript. |
@fatcerebus
I think that's possible. The It's just that with
So they should still be OK because their intended compile errors should still be triggered in their clients code. |
Ah, I missed the part where it doesn't apply to Now there is still the issue that the required implementation signature might in fact be too wide that the library writer doesn't want to expose it. I wonder how often it happens in practice that the implementation signature of a set of overloads is something a user would be comfortable making public... |
Could you please clarify with an example what you mean by Maybe the problem is with my statement
where (1) and (2) should be reversed. It should be:
Certainly it would be ideal for the compiler to type check, as far as possible, that the implementation and overload signatures, correspond correctly. There is an open issue for doing exactly that, at least in limited cases. Of source the catch-all case should also be checked if possible. (Looking for it ....). But this proposal does not extend to type checking the implementation against the overloads. Simple because of need to keep proposals simple. |
"Implementation signature" in reference to TypeScript overloads almost invariably refers to the last one that isn't visible to callers, containing the implementation of the function. If I've read your proposal correctly, you're proposing a new flag that forces the user to expose that signature publicly as a "cover case", but part of the reason it's not publicly exposed by default is that, generally, it ends up being too permissive for callers with no practical means of narrowing it down. For example say I write function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: string | number, y: string | number): string | number {
return x + y;
} I really, really don't want callers to be doing |
Without catch-all:
If you wrote the the lib with the proposal + additional proposals 1 & 2:
the the client may write
|
🔍 Search Terms
Most permissive catch-all case, cover, overload, gap, signature call error, require overload catch-all
This proposal overlaps #57004, but does not include a change from overload single matching to overload multiple matching. That makes it a much smaller change. Notably this proposal satisfies the viability checklist while 57004 did not.
✅ Viability Checklist
⭐ Suggestion
In current TypeScript, when the compiler is faced with an overload that does not include a "most permissive catch-all case" the compiler assumes that the user has declared by omission that the "most permissive catch-all case" could result in an unexpected error, and so emits a compile error to prevent that. (See Examples below.)
Note: Herein the "most permissive catch-all case" is called the Cover).
This proposal adds a new compiler flag:
When this flag is set, the compiler will check that the cover is declared as for overload declarations, or overload type declaration in the users source code, but not for overloads written in a declaration file (i.e., imported).
For back compatibility the default value of the flag is
false
.📃 Motivating Example
Example 1, Unecessary signature call errors
Example 1: Current behavior - toy case of unnecessary signature call error
With the
--requireOverloadCatchAll
flag set, the user would be required to write the cover case,and the error would not be emitted.
Example 2: Current behavior - hard to understand errors can be solved by writing cover case
A simple mapping overload function
results in an difficult to understand error when applied to the array map function:
With the
--requireOverloadCatchAll
flag set, the user would have been required to write the cover case,and the error would not have occurred.
Example 3: Current behavior - No cover case is not good practice for a library writer
Example 3a:
A simple mapping function. The library writer has declared the cover case in the documentation, but not in the implementation.
The library client gets a hard to understand signature call error message as shown:
Example 3b:
The error is a nuisance to the library client, because the library user has already declared the cover case in the documentation so there is no danger of a mistake.
With the
--requireOverloadCatchAll
flag set, the library writer would be required to write the cover case,and the client would not have encountered the error.
A counter argument is that the library writer might want to save a few characters and write the implementation without the catch all case, e.g.
and have the compiler emit an error, even if it troublesome for the user. That's possible, but it's not good practice for a library writer, so that is not a good argument.
Example 4: No cover case hobbles inheritance, and is not good practice for a library writer
Adapted from issue #56829.
With the
--requireOverloadCatchAll
flag set, the library writer would be required to write the cover case,which would enable the library client to successfully inherit.
💻 Use Cases
As show in the examples, enabling
--requireOverloadCatchAll
flag wouldAdditional Proposals that would be user friendly additions to the
--requireOverloadCatchAll
proposal.These addition proposals not abolute necessities, but would be user-friendly. It makes sense to attach them as an addendum to this proposal, because they are strongly related.
Additional Proposals Part 1:
#13219 (closed) is a superset of this (this doesn't include throw flow tracking). However, the decision to close 13219 t didn't consider it's overwhelming advantage in the context of overloads and their covers (that was part of the issue). The "throws" declaration provides the library user critical information about actions taken in the cover "catch-all" case.
Proposal: Add the ability to declare the thrown errors:
This would be visible to the library client in type display.
This proposal does NOT inlcude full throw flow tracking. It just help to ensure that the library clients known the library writers intention.
Additional Proposals Part 2
It's troublesome to write out the cover type for the last case, so an intrinsic could be added to assist:
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
indicates the type that should be thrown whengapReturnType
isthrows
, otherwise ignored.Instead of writing out the cover type for the last case like this:
the library writer could write:
Similarly to defined an overload type without a declaration:
SetupOverload
andCreateOverload
were also included in proposal #57004. Even though the--requireOverloadCatchAll
proposal does not include multiple matching, these intrinsics would be useful friendly for the current overload single matching algorithm as well.Update 1/26/2024 - Give
GapReturn
an additional choice:compilerError
, which corresponds to the current behavior when no catch-all case is included. Consider this scenario:The text was updated successfully, but these errors were encountered: