-
-
Notifications
You must be signed in to change notification settings - Fork 162
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
New strong-type EventDispatcher and Threejs strong-types events #369
Conversation
I'm not too expertised to see if it's conventionally a good solution. |
So just in a "eli5" way, you're passing what you expect the target to be now? e.g. like event handlers in react? |
ELI5:The Goal is Events in ThreeJs can be strongly typed and allow any other external lib or user of the three-types extends with his own custom strong events. Non ELI5:The goal is for Looking at the Typing of HTMLElement the strong typed Events are mapped via HTMLElementEventMap with a hierarchy of EventMap interfaces, and this Map's the Other changes are:
|
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.
Nice explanation, lovely addition.
|
||
export {}; |
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.
What is the purpose of these export
statements?
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.
Because the ArcballControlsEventMap
is not exported.
DefinitelyTyped rules complain that there is a non-exported type.
This is the way to tell the DefinitelyTyped rule that you want not to export something by default.
This is used in many places to allow not export the EventMap
Looks good from a first glance, will take a closer look soon. |
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.
Left some initial thoughts.
export interface Event<TSource = unknown, TEventType extends string = string> { | ||
readonly type: TEventType; | ||
readonly target: TSource; | ||
} |
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 TTarget
be more accurate than TSource
?
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.
Update with your suggestion.
type EventReceiver<TSource, TEventType extends string, TEventData> = ( | ||
event: TEventData & Event<TSource, TEventType>, | ||
) => void; |
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 keeping this called EventListener
and keeping the order of the generics the same as before to avoid the change in this type being a breaking change?
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.
Update with your suggestion.
type EventTypeValidator<TEvent extends BaseEvent, TEventMap extends {}> = TEvent extends { | ||
type: infer TEventType; | ||
} | ||
export type EventListener<E, T, U> = (event: E & { type: T } & { target: U }) => void; | ||
? TEventType extends EventKey<TEventMap> | ||
? { type: TEventType } & TEventMap[TEventType] | ||
: TEventType extends string | ||
? TEvent | ||
: never | ||
: never; |
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.
So this supports dispatching basically any event if the key does not exist in the event map. Are there common use-cases where this is necessary? Seems like it might be nice to have the ability to block any undeclared events.
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.
EventDispatcher
allow dispatch and listen for any event.
In my opinion, Typescript types should not block or reduce the features/capabilities of the Javascript code, only add helpful type information to reduce the errors caused by not using the correct typing.
Blocking unknown event types would reduce the features/capabilities of the original EventDispatcher
code.
I don't know how common is for people to use custom events on THREE.Object3D
or custom EventDispatcher
. but I imagine that some are. And I think EventDispatcher typing should allow them to decide if they want the custom event to be strong-typed or not.
I can imagine that I would prefer to create a simple quick and dirty custom Object3D
or other EventDispatcher
sub-type and test each feature without wanting to have to do the bureaucracy of creating custom strong-type events for a test.
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 think it's reasonable to want to block unknown event types if the user knows exactly what set of event types they want to allow.
It seems like we could support both approaches. For the places where we want the event type to be open, the event map could include an index signature. For the places where we want the event type to be closed, the event map could not include an index signature.
My problem with the current approach is that it makes it impossible to block unknown events, whereas relying on whether there's an index signature or not would allow for either approach.
Thoughts?
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 suppose there's no way to make a closed hierarchy of events in the way DOM events are typed either. Let me reconsider this.
// ts-expect-error | ||
eveDisForTestEvent.addEventListener('baz', e => { | ||
e.type; // $ExpectType "baz" | ||
e.target; // $ExpectType EventDispatcher<TestEvent> | ||
}); |
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.
It looks we're no longer producing an error in this case since @ts-expect-error
was changed to ts-expect-error
. Is that intentional?
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.
Sorry, that was an error. Tests are now updated and with better comments.
Sorry, I finally fixed the conflict. |
@rafaelsc I'm interested in completing this, but I don't want to step on your toes. Let me know if you plan to get back this, otherwise I will probably take this on. |
To follow-up on the discussion regarding whether to allow dispatching unknown events. After trying a few approaches, taking a more strict approach to events and allowing the user to loosen the typings seems like the best option. RationaleThe main reason for taking this approach is that it is the only safe approach. Consider a simplified interface Object3DEventMap {
added: {};
removed: {};
}
interface WebXRSpaceEventMap extends Object3DEventMap {
select: { data: XRInputSource };
}
class XRHandSpace extends Group<WebXRSpaceEventMap> {} If we allowed dispatching unknown events, it would allow dispatching a bad const handSpace = new THREE.XRHandSpace();
function dispatchSelectEvent(object: THREE.Object3D) {
object.dispatchEvent({ type: 'select' });
}
dispatchSelectEvent(handSpace); This breaks how inheritance and types should work and it seems best to at least provide the option of avoiding this issue. Dispatching unknown eventsBecause of that, we only allow dispatching defined events. There are a few ways to add events to the defined event map.
const allowSpecificEvent = new THREE.Object3D<THREE.Object3DEventMap & { test: { data: number } }>();
allowSpecificEvent.dispatchEvent({ type: 'test', data: 5 });
const allowAnyEvent = new THREE.Object3D<THREE.Object3DEventMap & Record<string, { [key: string]: unknown }>>();
allowAnyEvent.dispatchEvent({ type: 'any', data: 5 });
class AllowSpecificEvent extends Object3D<THREE.Object3DEventMap & { test: { data: number } }> {}
const allowSpecificEventClass = new AllowSpecificEvent();
allowSpecificEventClass.dispatchEvent({ type: 'test', data: 5 });
class AllowAnyEvent extends Object3D<THREE.Object3DEventMap & Record<string, { [key: string]: unknown }>> {}
const allowAnyEventClass = new AllowAnyEvent();
allowAnyEventClass.dispatchEvent({ type: 'any', data: 5 });
declare module 'three' {
interface Object3DEventMap {
[key: string]: { [key: string]: unknown };
}
}
const allowAnyEvent = new THREE.Object3D();
allowAnyEvent.dispatchEvent({ type: 'any', data: 5 }); ConclusionI'm hoping this change provides more good than harm, please let me know if this ends up causing a lot of churn and we can take a look at what we can do to make it better. |
BREAKING CHANGE
Why
What
Checklist
master
, next goesdev
)