-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Observable and EventData cleanup #10181
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
Conversation
☁️ Nx Cloud ReportCI is running/has finished running commands for commit 23a3498. As they complete they will appear below. Click to see the status, the terminal output, and the build insights. 📂 See all runs for this branch ✅ Successfully ran 2 targetsSent with 💌 from NxCloud. |
I've built locally and didn't expect to see test failures, so I'll dig into those. EDIT: the test failures seem like they may just be false-negatives regarding sub-pixel rendering in layout tests, so I expect they'll pass on CI even if they don't pass here. Asking advice in the TSC chat. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome to me as it is! 2 things i would love to see added:
- template type for EventData T extending Observable to define "object"
- adding
[k:string]:any
to EventData because most EventData are augmented with data props or others.
Thanks for the review, @farfromrefug! As for the wishlist:
I like it too in theory (have always wanted to just do But in practice, in most contexts (like defining a click handler in React or Svelte), unless it were inline, you'd still need to explicitly specify the generic argument in your callback anyway, so it'd be no less typing saved.
I see the use - though I think the proper solution for this is for the library-maker to properly type their event handlers (e.g. say that the event named "MyEvent" corresponds to the EventData extension, |
About the template type. A simple As for the other point I understand what you mean. It is just a pain to have tsc always giving errors about EventData properties while everyone know there is always other properties. |
I can see that that change would touch a lot of files though, so to reduce the review burden, I'll try preparing a draft PR now that depends on this one that implements just that extra step. |
@shirakaba it won't change anything! The default T makes it work without a single change ! |
f625f5b
to
3fb2146
Compare
It certainly wouldn't break anything, but... So I could do just that one-line change, but I don't see what it improves as we'd still have to be casting |
@shirakaba yes it would be best. But as I said I could already use it in extending EventData or in my callbacks. It is already a free and useful gain. |
3fb2146
to
032532b
Compare
@farfromrefug I've put together a follow-up commit to implement generic events - as you can see, doing it end-to-end is a big change on top of this PR: events-cleanup...generic-events It would potentially be a breaking change for libraries/plugins - any class that were subclassing Observable would now have to add those generic types to the signatures of anything handling event listeners. It probably wouldn't introduce any breaks for end-users. But as it may be a breaking change for libraries, let's keep this commit for generics out of this PR and review it in a follow-up PR. Don't want to have to roll back this cleanup PR just due to finding a problem with the generics! |
@shirakaba awesome. But I dont think it is a breaking change. You dont need to use generic as all methods / types have default generic. |
The full commit to implement the generic end-to-end would be a compile-time breaking change - playground: Any existing plugins extending Just implementing the one-line change |
abortedFlags.set(signal, true) | ||
signal.notify({ eventName: "abort", type: "abort" }) | ||
abortedFlags.set(signal, true); | ||
signal.notify({ eventName: 'abort', type: 'abort', object: this }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You dont need to pass object:this
here. It is added by default from notifiy
(if no object
set)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR actually removes the logic in Observable.prototype.notify
that fills in an empty object
field, so it's necessary (unless we decide to put that logic back in):
- public notify<T extends NotifyData>(data: T): void {
- const eventData = data as EventData;
- eventData.object = eventData.object || this;
+ public notify<T extends EventData>(data: T): void {
Demanding that object
be filled in allows us greatly simply the EventData typings. The user gets the same EventData out as they put in. No partials or optionals or anything, and no NotifyData vs. EventData. I think there's a great advantage to simplicity when the codebase has turned off strict type-checking.
In terms of performance as well, making the users pass in the EventData in its final shape rather than us potentially altering EventData, it may go (in some cases, a tiny bit) faster, as V8 won't have to re-assess its Shape upon the reassignment. Although maybe that's only when JIT is enabled...
But I can understand that it's less typing for the user, which is an advantage. I wonder whether this would be an okay middle-ground:
+ // Makes the specified properties in a type optional.
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
+
- public notify<T extends NotifyData>(data: T): void {
+ public notify<T extends EventData>(data: Optional<T, 'object'>): void {
- const eventData = data as EventData;
eventData.object = eventData.object || this;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shirakaba I missed that change but that would be a breaking change though. I know i have many plugins depending on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about the middle ground I suggested, though, which preserves the optional member?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied a solution in the commit fix: allow "object" to be optional in notify()
. Hope you like it!
export const off = global.NativeScriptGlobals.events.off.bind(global.NativeScriptGlobals.events); | ||
export const notify = global.NativeScriptGlobals.events.notify.bind(global.NativeScriptGlobals.events); | ||
export const hasListeners = global.NativeScriptGlobals.events.hasListeners.bind(global.NativeScriptGlobals.events); | ||
export const on = global.NativeScriptGlobals.events.on.bind(global.NativeScriptGlobals.events) as typeof import('.')['on']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't it be cleaner to do
import * as IApplication from '.';
...
export const on = global.NativeScriptGlobals.events.on.bind(global.NativeScriptGlobals.events) as typeof IApplication.on;
I have not "tested" this but i guess it should work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now done in the commit chore: cleaner types using IApplication
.
notify<T>(data: any): void; | ||
notifyPropertyChange(propertyName: string, value: any, oldValue?: any): void; | ||
hasListeners(eventName: string): boolean; | ||
[Key in keyof import('data/observable').Observable]: import('data/observable').Observable[Key]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as before. Wouldn't it be cleaner to import data/observable
as Observable from top and use it here?
maybe it wasn't working?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to check. I never have much luck defining imports in .d.ts
files (have to be very careful not to accidentally turn it into a module, causing it to lose its ambient context). But happy to give that a try.
... Side note, changes to this file are a moot point in the first place as the global types have been broken since @types/node
was updated to version 16, as they changed the way you have to augment the global
/globalThis
context. I have a branch fix/global-types
that fixes them that I haven't opened a PR for yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried out a few different approaches in a new minimal project using @types/node@14
(which still uses NodeJS.Global
).
❌ Attempt one: importing at top of file
This doesn't work (as expected), as the top-level import changes global-types.d.ts
into a module, so it stops filling ambient types.
❌ Attempt two: importing within the namespace
This doesn't work either. We'd expect events
to bear the eventOne
key, but it doesn't auto-complete.
⚠️ Attempt three: creating a type alias within the namespace
This does work, but it leaves some pollution - it now means that the users have to put up with NodeJS.Observable
being in tsc's compilation context. i.e. they can write const obs = new Observable as NodeJS.Observable
.
✅ Original attempt
The original suggestion gets working Intellisense without leaving any pollution in the user types.
So I think we'd best stick with the code in the PR!
@@ -533,6 +533,7 @@ export abstract class EditableTextBase extends EditableTextBaseCommon { | |||
|
|||
this.notify({ | |||
eventName: EditableTextBase.blurEvent, | |||
object: this, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed. Added by default and less JS code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now reverted.
6082a50
to
7528a50
Compare
a doubt, this will make the type in the event for example: loaded, come defined for the object property? |
This PR@vallemar This PR will behave as follows: The callback for the "loaded" event will return an export abstract class View extends ViewCommon {
// ...
on(event: 'loaded', callback: (args: EventData) => void, thisArg?: any);
// ...
}
export interface EventData {
eventName: string;
object: Observable;
} Thus, usage will look like this: new Button().on("loaded", (eventData: EventData) => {
const object = eventData.object as Button;
}); So, if you want to access any Button-specific APIs, you will still have to cast the Observable to a Button. But if you only need to call Observable APIs, then no type-casting is necessary. More than anything, the advantage is that the The end-goalThis PR paves the way for a follow-up PR (this commit: events-cleanup...generic-events ) where we'd introduce generics. That would improve the API to: new Button().on<Button>("loaded", (eventData) => {
eventData.object; // TypeScript infers this to be a Button
});
// Or (if I can set up the inference correctly by defaulting to the class itself):
new Button().on("loaded", (eventData) => {
eventData.object; // TypeScript infers this to be a Button
}); |
This PR cherry-picks some of the changes done in the course of #10100. It does not implement DOM Events; it just cleans up the existing Observable and EventData source code to reduce the changes needed for DOM Events support (whether implemented into Core or as a separate opt-in package).
PR Checklist
What is the current behavior?
Inconsistent EventData typings, with lots of
object?: Partial<Observable>
, andthisArg
not being passed along and/or correctly typed here and there.We also have conflicts between
observable/index.d.ts
andobservable/index.ts
.What is the new behavior?
Consistent, simplified, correct typings. One singular
observable/index.ts
.Fixes/Implements/Closes #[Issue Number].
No breaking changes anticipated.