Skip to content

Changes EventHandler<...> to have a this of type void.#3867

Merged
JoviDeCroock merged 3 commits into
preactjs:masterfrom
MicahZoltu:patch-2
Feb 1, 2023
Merged

Changes EventHandler<...> to have a this of type void.#3867
JoviDeCroock merged 3 commits into
preactjs:masterfrom
MicahZoltu:patch-2

Conversation

@MicahZoltu

@MicahZoltu MicahZoltu commented Jan 21, 2023

Copy link
Copy Markdown

Originally, this was set to E['currentTarget'] which, based on the MDN doc linked in #2166, certainly sounds like the correct thing to do. A couple years later it was changed to never in #3147 with no commentary on the PR or commit indicating why. My guess is that this was changed so arrow functions would work, which I believe are permanently bound and cannot be rebound by the caller.

I believe that never is wrong because this code doesn't compile:

declare const event: JSX.TargetedEvent<HTMLElement, Event>

declare const apple: JSX.EventHandler<JSX.TargetedEvent<HTMLElement, Event>>
apple(event) // error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'never'.

function Apple(model: Pick<JSX.HTMLAttributes<HTMLElement>, 'onInput'>) {
	model.onInput && model.onInput(event) // error TS2684: The 'this' context of type 'Pick<HTMLAttributes<HTMLElement>, "onInput">' is not assignable to method's 'this' of type 'never'.
}

Changing to void resolves this error and I believe will work everywhere that never worked.

I removed the comment since it no longer applies with this being never or void. It appears that it was just forgotten when the switch from E['currentTarget'] to never was made.

Originally, `this` was set to `E['currentTarget']` which, based on the MDN doc linked in preactjs#2166, certainly sounds like the correct thing to do.  A couple years later it was changed to `never` in preactjs#3147 with no commentary on the PR or commit indicating why.  My guess is that this was changed so arrow functions would work, which I believe are permanently bound and cannot be rebound by the caller.

I believe that `never` is wrong because this code doesn't compile:
```ts
type TargetedEvent<Target extends EventTarget = EventTarget, TypedEvent extends Event = Event> = Omit<TypedEvent, 'currentTarget'> & { readonly currentTarget: Target }
interface EventHandler<E extends TargetedEvent> {
	(this: never, event: E): void
}
declare const apple: EventHandler<TargetedEvent<HTMLElement, Event>>
declare const event: TargetedEvent<HTMLElement, Event>
apple(event) // error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'never'.
```
Changing to `void` resolves this error and should work everywhere I believe.

I removed the comment since it no longer applies with this being `never` or `void`.  It appears that it was just forgotten when the switch from `E['currentTarget']` to `never` was made.
@robertknight

robertknight commented Jan 31, 2023

Copy link
Copy Markdown
Member

A couple years later it was changed to never in #3147 with no commentary on the PR or commit indicating why.

@JoviDeCroock can you remember the rationale for the change?

Edit: There is some context in #3137.

Personally I would like to amend the TypeScript types to reflect this, and simply have the context of events be never or some value that implies it is not to be used.

You can't call a function if it's this is set to never, since TS will infer the type as void, so that definitely seems preferable if we wanted to prevent users from relying on using this.

@MicahZoltu

Copy link
Copy Markdown
Author

You can't call a function if it's this is set to never, since TS will infer the type as void, so that definitely seems preferable if we wanted to prevent users from relying on using this.

I believe that void addresses this problem as void can't actually be used for anything, similar to never.

@JoviDeCroock

JoviDeCroock commented Feb 1, 2023

Copy link
Copy Markdown
Member

Hey, this was a big miss on my behalf. The failing test is what I think lead us down the path of changing this. Going to fish our issues in a bit. When trying it out in the browser logging out this seems to be the Element when using function and window when using () => {} I think that was the reasoning behind making it never.

EDIT: this was one of the original issues #3137 I wonder whether we can make it the element 😅

@MicahZoltu

Copy link
Copy Markdown
Author

I get window for both when the function is not bound to anything.
image

When you bind an arrow function however, the binding is ignored (this always points to globalThis):
image

The underlying problem here is that JavaScript functions can be rebound. Just because this is the element initially, doesn't mean that will be the case if someone calls the function elsewhere unless the function is explicitly bound.
image
image

The net takeaway here I think is that relying on this being the element could be safe, but preact would need to take care to ensure it doesn't accidentally rebind this along the way (which is easy to do if you are using classes).

@MicahZoltu

Copy link
Copy Markdown
Author

Reviewing the failing test, it appears that the test is incorrect here:

// Test `this` binding on event handlers
function onHandler(this: HTMLInputElement, event: any) {
	return this.value;
}
const foo = <input onChange={onHandler} />;

Specifically, if we want to say that there are no guarantees that this is the element in these callbacks then the test is correctly failing now while it was incorrectly passing previously. In the test case, if this was not an element the user would get an error about trying to access a property on undefined.

On the other hand, if we want to guarantee that this is the element, then we should switch the signature back to E['currentTarget'], and do whatever work is necessary to ensure that guarantee is made.

I'm happy to update the PR to "fix the test" (delete it), or switch back to E['currentTarget'].

@JoviDeCroock

Copy link
Copy Markdown
Member

Let's delete the test, I think we should be good to tell folks not to rely on this here

The type signature's intent is to make it clear to users that they cannot rely on `this` being the element in callbacks, and they should use `event.currentTarget` instead.  This test was testing that the user could unwisely ignore that.
@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage: 99.531%. Remained the same when pulling 150b1b0 on MicahZoltu:patch-2 into 15913ad on preactjs:master.

@MicahZoltu

Copy link
Copy Markdown
Author

Let's delete the test, I think we should be good to tell folks not to rely on this here

Done, and tests passing.

@JoviDeCroock JoviDeCroock merged commit e703a62 into preactjs:master Feb 1, 2023
@MicahZoltu MicahZoltu deleted the patch-2 branch February 6, 2023 16:21
JoviDeCroock added a commit that referenced this pull request Jan 12, 2024
JoviDeCroock added a commit that referenced this pull request Jan 12, 2024
* backport #3871

* port test from #3884 functionality seems to work in v11

* backport #3880

* backport #3875

* backport #3862

* add todo for #3801

* backport #3868

* backport #3867

* backport #3863

* add todo for #3856

* backport #3844

* backport #3816

* backport #3888

* backport #3889
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.

4 participants