Why is Event.target not Element in Typescript?
Asked Answered
M

12

250

I simply want to do this with my KeyboardEvent

var tag = evt.target.tagName.toLowerCase();

While Event.target is of type EventTarget, it does not inherit from Element. So I have to cast it like this:

var tag = (<Element>evt.target).tagName.toLowerCase();

This is probably due to some browsers not following standards, right? What is the correct browser-agnostic implementation in TypeScript?

P.S. I am using jQuery to capture the KeyboardEvent.

Mucoid answered 6/3, 2015 at 13:43 Comment(1)
A bit cleaner syntax var element = ev.target as HTMLElementPaleopsychology
O
134

It doesn't inherit from Element because not all event targets are elements.

From MDN:

Element, document, and window are the most common event targets, but other objects can be event targets too, for example XMLHttpRequest, AudioNode, AudioContext, and others.

Even the KeyboardEvent you're trying to use can occur on a DOM element or on the window object (and theoretically on other things), so right there it wouldn't make sense for evt.target to be defined as an Element.

If it is an event on a DOM element, then I would say that you can safely assume evt.target. is an Element. I don't think this is an matter of cross-browser behavior. Merely that EventTarget is a more abstract interface than Element.

Further reading: https://github.com/Microsoft/TypeScript/issues/29540

Octa answered 6/3, 2015 at 14:26 Comment(9)
In that case KeyboardEvent and MouseEvent should have it's own equivalent of EventTarget that will always contain the associated Element. DOM is so dodgy... :/Mucoid
@Mucoid Sounds like you're blaming DOM for limitations in TypeScript's type system. People using plain Javascript don't have any trouble using Event.target.Octa
I am not an expert on DOM nor TypeScript but I would say the design of the EventTarget has too much ambiguity and that has nothing to do with TypeScript.Mucoid
@Mucoid I would say you're only seeing it that way because you're viewing everything in terms of static types, and Javascript doesn't have static types. There is nothing ambiguous about event targets. An event target is whatever object where an event occurs. If the event was registered on an element, then the event target is that element. If the event was registered on an XHR, then the event target is that XHR. It's a simple and generalized convention that is applicable to a wide range of uses.Octa
@Mucoid I'd say it's best not to think of EventTarget as a type. It's really an interface for any object that provides the three methods for registering, unregistering, and dispatching events. The real "ambiguity" is with Event.target. The designers of TypeScript's type system could have defined Event as a generic class Event<T extends EventTarget> that has a target property of type T, but they didn't, and that's why you have to do an explicit cast. So like I said, your beef is with TypeScript, not with the DOM.Octa
@Mucoid On the other hand, KeyboardEvents can occur on both DOM elements and on the window object (and theoretically other things), so right there it's impossible to give KeyboardEvent.target a type that's any more specific than EventTarget, unless you think KeyboardEvent should also be a generic type KeyboardEvent<T extends EventTarget> and would like to be forced to put KeyboardEvent<Element> all throughout your code. At that point, you're better off just doing the explicit cast, painful though it may be.Octa
In cases it's helpful for anyone else in the future, I needed to cast as a specific element type in order to access the value property of a <select> tag. e.g. let target = <HTMLSelectElement> evt.target;Abase
@Abase (unfortunately) that is the correct way to handle ambiguities in a typed environment.Curet
I'm curious whether KeyboardEvent can just be used as is, or am I supposed to use KeyboardEvent<HTMLInputElement>? I find that if it gets used like this, then I still have to cast the e.target as HTMLInputElement.Abettor
T
170

JLRishe's answer is correct, so I simply use this in my event handler:

if (event.target instanceof Element) { /*...*/ }
These answered 14/5, 2018 at 9:6 Comment(5)
This is the most typesafe solution!Carmancarmarthen
This is actually the best solution as it actually checks the target at runtimeFarrar
Perfect solution and should be marked as the answer. I was looking for a good solution when adding a click handler using <svelte:window> with the Svelte framework and this takes care of it.Hazlett
Note that this way, the listener is not called if the event is triggered on a non-element target. For example: document.body.firstChild.click() (the first child is often an empty Text Node)Lenardlenci
Can I replace Element with HTMLElement ?Instigation
O
134

It doesn't inherit from Element because not all event targets are elements.

From MDN:

Element, document, and window are the most common event targets, but other objects can be event targets too, for example XMLHttpRequest, AudioNode, AudioContext, and others.

Even the KeyboardEvent you're trying to use can occur on a DOM element or on the window object (and theoretically on other things), so right there it wouldn't make sense for evt.target to be defined as an Element.

If it is an event on a DOM element, then I would say that you can safely assume evt.target. is an Element. I don't think this is an matter of cross-browser behavior. Merely that EventTarget is a more abstract interface than Element.

Further reading: https://github.com/Microsoft/TypeScript/issues/29540

Octa answered 6/3, 2015 at 14:26 Comment(9)
In that case KeyboardEvent and MouseEvent should have it's own equivalent of EventTarget that will always contain the associated Element. DOM is so dodgy... :/Mucoid
@Mucoid Sounds like you're blaming DOM for limitations in TypeScript's type system. People using plain Javascript don't have any trouble using Event.target.Octa
I am not an expert on DOM nor TypeScript but I would say the design of the EventTarget has too much ambiguity and that has nothing to do with TypeScript.Mucoid
@Mucoid I would say you're only seeing it that way because you're viewing everything in terms of static types, and Javascript doesn't have static types. There is nothing ambiguous about event targets. An event target is whatever object where an event occurs. If the event was registered on an element, then the event target is that element. If the event was registered on an XHR, then the event target is that XHR. It's a simple and generalized convention that is applicable to a wide range of uses.Octa
@Mucoid I'd say it's best not to think of EventTarget as a type. It's really an interface for any object that provides the three methods for registering, unregistering, and dispatching events. The real "ambiguity" is with Event.target. The designers of TypeScript's type system could have defined Event as a generic class Event<T extends EventTarget> that has a target property of type T, but they didn't, and that's why you have to do an explicit cast. So like I said, your beef is with TypeScript, not with the DOM.Octa
@Mucoid On the other hand, KeyboardEvents can occur on both DOM elements and on the window object (and theoretically other things), so right there it's impossible to give KeyboardEvent.target a type that's any more specific than EventTarget, unless you think KeyboardEvent should also be a generic type KeyboardEvent<T extends EventTarget> and would like to be forced to put KeyboardEvent<Element> all throughout your code. At that point, you're better off just doing the explicit cast, painful though it may be.Octa
In cases it's helpful for anyone else in the future, I needed to cast as a specific element type in order to access the value property of a <select> tag. e.g. let target = <HTMLSelectElement> evt.target;Abase
@Abase (unfortunately) that is the correct way to handle ambiguities in a typed environment.Curet
I'm curious whether KeyboardEvent can just be used as is, or am I supposed to use KeyboardEvent<HTMLInputElement>? I find that if it gets used like this, then I still have to cast the e.target as HTMLInputElement.Abettor
A
84

Using typescript, I use a custom interface that only applies to my function. Example use case.

  handleChange(event: { target: HTMLInputElement; }) {
    this.setState({ value: event.target.value });
  }

In this case, the handleChange will receive an object with target field that is of type HTMLInputElement.

Later in my code I can use

<input type='text' value={this.state.value} onChange={this.handleChange} />

A cleaner approach would be to put the interface to a separate file.

interface HandleNameChangeInterface {
  target: HTMLInputElement;
}

then later use the following function definition:

  handleChange(event: HandleNameChangeInterface) {
    this.setState({ value: event.target.value });
  }

In my usecase, it's expressly defined that the only caller to handleChange is an HTML element type of input text.

Ahrens answered 25/1, 2018 at 13:16 Comment(3)
This worked perfectly for me - I was trying all sorts of nastiness, extending EventTarget etc. but this is the cleanest solution +1Enzymology
Just to add to this, if you need to extend the event definition you can do something like this: handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement> & { target: HTMLInputElement }) => {...}Enzymology
The custom interface destroys the rest of the information about the Event object. @Enzymology 's example is additive and much better.Singlehearted
P
63

Typescript 3.2.4

For retrieving property you must cast target to appropriate data type:

e => console.log((e.target as Element).id)
Palpebrate answered 24/1, 2019 at 16:58 Comment(5)
Is that the same as the <HTMLInputElement>event.target; syntax?Dendrology
@KonradViltersten, they do the same thing. The as syntax was introduced because it conflicted with JSX. It's recommended to use as for consistency. basarat.gitbooks.io/typescript/docs/types/type-assertion.htmlClive
Aha, I see. It's also appearing more C#'ish which in many cases is an advantage, depending on the team's backend experience. As long as it's not one of those false friends where the syntax resembles something but implies something totally different technically. (I'm thinking var and const between Angular and C#, a sad experience of mine, hehe).Dendrology
@KonradViltersten unrelated to the original question, on the matter of appearance of c#, maybe because of the great influence in the eventual development of both languages since they're both Microsoft's and one person being high involved in the development, ie Anders Hejlsberg as the lead architect of C# and core developer on TypeScript. Though I agree it's not great to have the same syntax mean different things in a very similar language. 😂 en.wikipedia.org/wiki/Anders_HejlsbergAhrens
Using as should be a last resort if you do not want a liar liar pants on fire codebase.Singlehearted
C
22

Could you create your own generic interface that extends Event. Something like this?

interface DOMEvent<T extends EventTarget> extends Event {
  readonly target: T
}

Then you can use it like:

handleChange(event: DOMEvent<HTMLInputElement>) {
  this.setState({ value: event.target.value });
}
Chickenhearted answered 24/3, 2020 at 14:34 Comment(3)
huh, glad you still maintain it, thanks :) Not that readonly was required there, but a little extra type safety never hurts (I hope no one will try to override the target property, but you never know, I've seen worse).Aideaidedecamp
type DOMEvent<E extends Event, T extends Element> = E & { readonly target: T; }; Here's a variation on the above that doesn't clobber the original Event type. e.g. DOMEvent<TouchEvent, HTMLButtonElement> will still correctly autocomplete for event.touches. It feels like a missed detail that Event cannot infer target type from the target that the event handler is bound onto.Singlehearted
see also React.MouseEvent<HTMLDivElement> etc here and hereErenow
G
3

I use this:

onClick({ target }: MouseEvent) => {
    const targetElement: HTMLElement = target as HTMLElement;
    
    const listFullHeight: number = targetElement.scrollHeight;
    const listVisibleHeight: number = targetElement.offsetHeight;
    const listTopScroll: number = targetElement.scrollTop;
    }
Gaggle answered 30/11, 2020 at 7:50 Comment(2)
What if they don't click on a <div>?Heartstrings
For a simple onclick on a button I am willing to use this and avoid the type spaghetti of the alternativesKatlaps
I
3

For Angular 10+ Users

Just declare the HTML Input Element and extend it to use the target as an object as I did below for my bootstrap 4+ file browser input. This way you can save a lot of work.

  selectFile(event: Event & { target: HTMLInputElement}) {
    console.log(event.target.files);
    this.selectedFile = event.target.files[0];
  }
Ingleside answered 27/7, 2021 at 16:46 Comment(2)
I'm not sure about event types in Angular, but shouldn't your event be some kind of Event type instead of HTMLInputElement ?Euthenics
This technique only seems to work if you turn off strictTemplates in angularCompilerOptions in the tsconfig.json file.Truong
P
2

With typescript we can leverage type aliases, like so:

type KeyboardEvent = {
  target: HTMLInputElement,
  key: string,
};
const onKeyPress = (e: KeyboardEvent) => {
  if ('Enter' === e.key) { // Enter keyboard was pressed!
    submit(e.target.value);
    e.target.value = '';
    return;
  }
  // continue handle onKeyPress input events...
};
Papa answered 22/3, 2020 at 2:28 Comment(0)
P
1

@Bangonkali provide the right answer, but this syntax seems more readable and just nicer to me:

eventChange($event: KeyboardEvent): void {
    (<HTMLInputElement>$event.target).value;
}
Phonolite answered 11/11, 2019 at 18:34 Comment(0)
S
1

As JLRishe correctly said in his answer, the target field of the Event object does not have to be an Element. However, we can be sure that any event that inherits from UIEvent will always have a target field in the listener with a type that implements the Node interface, with the exception of only one case for Windows. I would present this exception this way (using the example of a click event):

window.addEventListener('click', e => {
  console.log(e.target)                      // Window
  console.log(e.target instanceof Node)      // false
}
window.dispatchEvent(new MouseEvent('click'))

In all other cases, it will be any type inherited from Node.

Why Node and not Element?

That's a good question. It is fair to say that if an event is triggered natively by user interaction with the GUI, then it will always be a type implementing Element. But it still remains possible to call any UIEvent artificially:

const node = document.createTextNode('')
document.body.appendChild(node)
window.addEventListener('click', e => {
    console.log(e.target instanceof Element))    // false
}
node.dispatchEvent(new MouseEvent('click', {bubbles: true}))

And although this example is from a spherical vacuum, it is possible.

All this is very interesting, but difficult. And that's why I suggest using a package that provides for all possible cases and automatically outputs safe types. That's package is types-spring. For clarity:

listener is HTMLElement

original types:
const elem = document.querySelector('div')
if (elem) elem.addEventListener('click', e => {

    e.target                               // EventTarget| null
    if (e.currentTarget)
    {
         e.currentTarget                   // EventTarget
    }
})
with types-spring:
const elem = document.querySelector('div')
if (elem) elem.addEventListener('click', e => {

    e.target                               // Node | null
    if (e.currentTarget)
    {
         e.currentTarget                   // HTMLDivElement 
    }
})

Listener is Window

original types:
window.addEventListener('click', e => {
    if (e.target) {
        if (e.isTrusted === true) {
            e.target                       // is EventTarget
        }

        if (e.target instanceof Window) {
            e.target                       // is Window
        }
        else {
            e.target                       // is EventTarget
        }
    }
})
with types-spring:

window.addEventListener('click', e => {
    if (e.target) {
        if (e.isTrusted === true) { 
            e.target                       // is Element
        }

        if ('atob' in e.target) {
            e.target                       // is Window
        }
        else {  
            e.target                       // is Node
        }        
    }
})

I hope the package helps with routine development

PS: Any ideas about this one?

Swiss answered 13/6, 2023 at 11:43 Comment(0)
H
0

I'm usually facing this problem when dealing with events from an input field, like key up. But remember that the event could stem from anywhere, e.g. from a keyup listener on document, where there is no associated value. So in order to correctly provide the information I'd provide an additional type:

interface KeyboardEventOnInputField extends KeyboardEvent {
  target: HTMLInputElement;
}
...

  onKeyUp(e: KeyboardEventOnInputField) {
    const inputValue = e.target.value;
    ...
  }

If the input to the function has a type of Event, you might need to tell typescript what it actually is:

  onKeyUp(e: Event) {
    const evt = e as KeyboardEventOnInputField;
    const inputValue = evt.target.value;
    this.inputValue.next(inputValue);
  }

This is for example required in Angular.

Hissing answered 4/2, 2021 at 8:37 Comment(0)
S
0

Answer from https://mcmap.net/q/101628/-why-is-event-target-not-element-in-typescript and https://mcmap.net/q/101628/-why-is-event-target-not-element-in-typescript is correct, but I got a nicer way to do it. For example


  // in another file
  export interface DOMEvent<T extends EventTarget> extends Event {
  readonly target: T;
  }


  onFileChange(event: Event): void {
    const { target } = event as DOMEvent<HTMLInputElement>;
    if (target.files && target.files.length > 0) {
      // do something with the target
    }
  }

Shanteshantee answered 11/11, 2022 at 4:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.