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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Feb 1, 2023

Conversation

MicahZoltu
Copy link
Contributor

@MicahZoltu MicahZoltu commented Jan 21, 2023

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
Copy link
Member

robertknight commented Jan 31, 2023

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
Contributor 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
Copy link
Member

JoviDeCroock commented Feb 1, 2023

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
Contributor 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
Contributor 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
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

Coverage Status

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

@MicahZoltu
Copy link
Contributor 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