Skip to content

node: events for v25#73882

Closed
Renegade334 wants to merge 5 commits intoDefinitelyTyped:masterfrom
Renegade334:node-events-v25
Closed

node: events for v25#73882
Renegade334 wants to merge 5 commits intoDefinitelyTyped:masterfrom
Renegade334:node-events-v25

Conversation

@Renegade334
Copy link
Contributor

@Renegade334 Renegade334 commented Oct 12, 2025

What could go wrong..?

POC for two sets of EventEmitter changes for v25.

The first (4ebdaa4) is a general cleanup of the events module that I've been meaning to get round to for a while. This also contains alterations to the event map behaviour of EventEmitter, mostly along the lines of #71060, with some minor tweaks. This is working well – the only breakage in the DT tests is that EventEmitter no longer plays with Function types, which was a deliberate choice (but can always be revisited).

Aside from some serious simplification, the main advantage of the event map changes is that EE now permits events not in the event map and assumes them to have a default (...args: any[]) => void listener type, which plays a lot better inheritance-wise.

The second (7f09a01) is a new paradigm for internal event emitters. These can't use EventEmitter<...> event maps, as deep assignability in dark corners of the ecosystem relies on these methods resolving to a permissive signature accepting any string | symbol and (...args: any[]) => void combo. However, we can still do better than the current approach, which is to define a hodge-podge of EventEmitter overrides with some event-specific overloads on each and every EventEmitter in the API. The old approach has many issues:

  • There is no automatic inheritance hierarchy for events: an event emitter D that inherits from C -> B -> A -> EventEmitter would have to define overloads for each and every event from A, B and C in addition to its own. In practice, this was rarely executed correctly.
  • The decision as to which methods were overridden was highly variable. Usually we'd provide the core ones (addListener, on etc.) but whether others like off, removeListener etc. were also overridden was essentially random.
  • The need to add an overload for each event to each overloaded function meant the list of overloads grew as k * n, and some of these blocks were 100+ lines long.

The new approach is an InternalEventEmitter pattern, which provides event-mapped type-hinting behaviour while still maintaining the same permissive behaviour as before. This should be very much non-breaking, and indeed caused no complaints in the DT tests.

Events for internal emitters are now truly hierarchical: a derived emitter D can have a DEventMap that inherits from CEventMap -> BEventMap -> etc., ensuring consistency and deduplication.

@typescript-bot typescript-bot moved this to Needs Author Action in Pull Request Status Board Oct 12, 2025
@Renegade334 Renegade334 force-pushed the node-events-v25 branch 10 times, most recently from 6eb8826 to 2acffd3 Compare October 12, 2025 21:02
Renegade334 and others added 5 commits October 13, 2025 12:24
Co-authored-by: ckohen <chaikohen@gmail.com>
`emit()` on internal emitters accepts any input, so these aren't
testing anything at all, as evidenced by the fact that a lot of these
are passing supposedly invalid input without errors.
@DangerBotOSS
Copy link

Formatting

The following files are not formatted:

  1. types/node/test/net.ts

Consider running pnpm dprint fmt on these files to make review easier.

Generated by 🚫 dangerJS against 68d642c

* @since v0.1.26
*/
class EventEmitter<T extends EventMap<T> = DefaultEventMap> {
class EventEmitter<T extends EventMap<T> = any> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This now has to be any rather than {} due to the miracle that is exactOptionalPropertyTypes. Interassignability between class instances has to match the type parameters, and with exOPT enabled, the compiler will complain that the properties provided in the event map are "missing" from NoEventMap when trying to assign an EventEmitter<T> to EventEmitter.

To be fair, any does rather accurately capture the behaviour of the default EventEmitter when it comes to event types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am glad EOPT caught this, but, this feels very odd; do things actually still work when any??

Copy link
Contributor Author

@Renegade334 Renegade334 Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do, in that both {} and any pass the constraint of a type that is assignable to Record<keyof T, any[]> but which also satisfies the conditional {} extends T, which is the gatekeeper for EE using the conventional rather than the event-mapped signatures.

declare const any: any;

{
using abortListener = events.addAbortListener(new AbortController().signal, (event) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside, I do believe that this is the first explicit resource management test in DT... 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants