Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic events #10193

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Generic events #10193

wants to merge 2 commits into from

Conversation

shirakaba
Copy link
Contributor

@shirakaba shirakaba commented Feb 1, 2023

With this PR, EventData and methods for adding/removing listeners will now be generic:

// This event is inherited from View, so eventData.object is
// inferred to be a View.
new Button().on("loaded", (eventData) => {
  eventData.object; // TypeScript infers this to be a View
});

// You can, however, explicitly pass the generic to narrow
// that down to Button:
new Button().on<Button>("loaded", (eventData) => {
  eventData.object; // TypeScript infers this to be a Button
});

// Button-specific events don't require passing the generic:
new Button().on("tap", (eventData) => {
  eventData.object; // TypeScript infers this to be a Button
});

Please do give comment on the discussion points I've posted and (once we've reviewed it and prepared a build of it) a try on your projects, as I fully expect it'll result in some TypeScript grumbles.

PR Checklist

What is the current behavior?

Types for EventData and adding/removing listeners are non-generic, so we always have to write eventData.object as Button if we want anything more specific than an Observable.

What is the new behavior?

Improved inference in most cases. The events in Application have always been very messy though, so I've left those as defaulting to Observable even though some could be narrowed to View, as I simply can't get my head round them.

Note that this PR would introduce breaking changes for library authors. As although we do provide default args for each generic, if they've extended on anywhere, they'll have to add the generic, as demonstrated in this playground:

image

For discussion

What should the generics be?

The generics implemented in this PR are "better than nothing", but still not ideal. See what I ideally wanted to implement vs. what I was able to get working:

// What I wanted:
export abstract class View extends ViewCommon {
  on<T extends View = this>(event: 'loaded', callback: (args: EventData<T>) => void, thisArg?: any);
}

// The only thing I could get to work:
export abstract class View extends ViewCommon {
  on<T extends Observable = View>(event: 'loaded', callback: (args: EventData<T>) => void, thisArg?: any);
}

The former is more strictly correct, but I just couldn't get TypeScript to accept it 馃し

Should we narrow the default generic for EventData in the first place?

Although I think it is correct for the methods for adding/removing event listeners to have more narrow generic defaults than just Observable, e.g.:

export class SegmentedBar extends View implements AddChildFromBuilder, AddArrayFromBuilder {
  on<T extends Observable = SegmentedBar>(event: 'selectedIndexChanged', callback: (args: SelectedIndexChangedEventData<T>) => void, thisArg?: any): void;
}

... I am unsure about whether the EventData interfaces should behave the same:

// Would we prefer a narrow default?
export interface SelectedIndexChangedEventData<T extends Observable = SegmentedBar> extends EventData<T> {
  oldIndex: number;
  newIndex: number;
}

// ... or just stick to Observable?
export interface SelectedIndexChangedEventData<T extends Observable = Observable> extends EventData<T> {
  oldIndex: number;
  newIndex: number;
}

Mainly because we don't know whether others might repurpose the same event (SelectedIndexChangedEvent can be used for more than just SegmentedBar, and indeed there are duplicates of this interface across the codebase). Of course consumers can just specify the generic explicitly if they want to override the assumed default of SegmentedBar, so it's not the end of the world whichever choice we go with.

@nx-cloud
Copy link

nx-cloud bot commented Feb 1, 2023

鈽侊笍 Nx Cloud Report

We didn't find any information for the current pull request with the commit 9a07b1b.
You might need to set the 'NX_BRANCH' environment variable in your CI pipeline.

Check the Nx Cloud Github Integration documentation for more information.


Sent with 馃拰 from NxCloud.

@cla-bot cla-bot bot added the cla: yes label Feb 1, 2023
@ammarahm-ed
Copy link
Sponsor Contributor

ammarahm-ed commented Feb 15, 2023

This is ok but I think it doesn't really solve the problem. All Views "Should" infer to correct types in events automatically without passing some Generic or anything.

new Button().on<Button>("loaded", (eventData) => {
  eventData.object; // TypeScript infers this to be a Button
});

is almost as much work as doing

new Button().on("loaded", (eventData) => {
  eventData.object as Button;
});

The ideal way would be to not have to pass any generic at all when you are subscribing to an event on Button. For example:

new Button().on("loaded", (eventData) => {
  eventData.object // is Button automatically
});

Also my question, why does this work or to be more specific, how does this work:

// Button-specific events don't require passing the generic:
new Button().on("tap", (eventData) => {
  eventData.object; // TypeScript infers this to be a Button
});

@farfromrefug
Copy link
Collaborator

@ammarahm-ed be careful some views fire event for which the object is not them self! I know some plugins already do that. And I think some N components also do that. Will try to find them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants