Ensure forward-compatibility of callbacks
See original GitHub issueSuggestion
š Search Terms
strict callback arguments, too many arguments warning, option
ā Viability Checklist
My suggestion meets these guidelines:
- 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 feature would agree with the rest of TypeScriptās Design Goals.
ā Suggestion
Edit: My original suggestion was unworkable. This is a second attempt.
const nextFrame = new Promise(requestAnimationFrame);
The above results in requestAnimationFrame
being called with two arguments rather than the one it accepts. This is fragile, since, in future, a second argument may be added to requestAnimationFrame
which makes the above code behave differently.
This problem is worse when it comes to browser-provided functions, as those change with new browser versions, and donāt rely on an app redeploy.
This can be worked around using never
:
declare function requestAnimationFrame(callback: FrameRequestCallback, _: never): number;
ā¦but it makes for a messy auto-complete, as the āneverā arg shows up.
A solution would be some way to mark a function as ācannot be assigned to a function type with more parametersā. This would allow library authors to make this assertion if they want to reserve additional params for future use, and this assertion could be added to DOM functions like requestAnimationFrame
.
š» Use Cases
I wrote a blog post about the risks with this pattern, including a section on how TypeScript doesnāt prevent it https://jakearchibald.com/2021/function-callback-risks/
Issue Analytics
- State:
- Created 3 years ago
- Reactions:48
- Comments:15 (4 by maintainers)
I donāt think my proposed solution is right, as itād cause TypeScript to reject things like this:
Line 2 is fine, because
doubler
is designed to be a callback toarray.map
, whereas line 3 is bad becauserequestAnimationFrame
isnāt designed to be a callback tonew Promise
. However, in both cases thereās a function being called with an incorrect number of arguments.Maybe this isnāt a typing problem, but Iāll leave this open in case anyone thinks of a better solution.
Edit: Iāve changed the solution in the OP.
I donāt know what to say except that those people are, factually, wrong. If a library author wants to keep the door open for adding parameters in the future in a way that isnāt a breaking change, they need to
throw
today if the incoming arity is too high.By way of analogy, most library authors donāt treat āaddingā a return value (e.g. going from implicit
undefined
tonumber
) to be a breaking change, but if tomorrowconsole.log
starts returning the number of characters printed likeArray#push
does, then this will break code likefoo(console.log(x) || x)
. TypeScript stops you from doing that because we think theconsole.log
return value is incidentallyundefined
(i.e.,void
) rather than intentionallyundefined
, and we get bug reports about this because people think itās safe to rely on that function always producing a falsy value, and I think on balance theyāre correct and we should changeconsole.log
ās return type to beundefined
because thatās whatās in the spec today and, barring evidence to the contrary, the spec for any given function isnāt likely to change.Ultimately the entire surface API of anything is subject to Hyrumās law and Iām struggling to draw a bright line where things ācould plausibly changeā and ācouldnāt plausibly changeā in a way that doesnāt require arguing about thousands of different functions in the DOM and core lib to represent this. Do we think
Array#forEach
ās callback could ever gain an additional arg? Why / why not? How do I apply that logic elsewhere?