238

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.

daniel.sedlacek
  • 8,129
  • 9
  • 46
  • 77

12 Answers12

149

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

if (event.target instanceof Element) { /*...*/ }
Simon Epskamp
  • 8,813
  • 3
  • 53
  • 58
126

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

Kyle W
  • 13
  • 4
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • 16
    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... :/ – daniel.sedlacek Mar 06 '15 at 14:59
  • @daniel.sedlacek 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`. – JLRishe Mar 06 '15 at 15:31
  • 9
    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. – daniel.sedlacek Mar 06 '15 at 16:43
  • @daniel.sedlacek 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. – JLRishe Mar 06 '15 at 16:47
  • 1
    @daniel.sedlacek 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` 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. – JLRishe Mar 06 '15 at 16:56
  • 4
    @daniel.sedlacek On the other hand, `KeyboardEvent`s 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` and would like to be forced to put `KeyboardEvent` all throughout your code. At that point, you're better off just doing the explicit cast, painful though it may be. – JLRishe Mar 06 '15 at 17:13
  • 18
    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 ` – munsellj Jul 28 '16 at 18:11
  • 2
    @munsellj (unfortunately) that is the correct way to handle ambiguities in a typed environment. – pilau Aug 18 '16 at 07:41
  • I'm curious whether `KeyboardEvent` can just be used as is, or am I supposed to use `KeyboardEvent`? I find that if it gets used like this, then I still have to cast the `e.target` as `HTMLInputElement`. – CMCDragonkai May 22 '19 at 06:33
81

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.

Bangonkali
  • 1,111
  • 9
  • 13
  • This worked perfectly for me - I was trying all sorts of nastiness, extending EventTarget etc. but this is the cleanest solution +1 – Kitson Nov 27 '19 at 09:29
  • 5
    Just to add to this, if you need to extend the event definition you can do something like this: `handleKeyUp = (event: React.KeyboardEvent & { target: HTMLInputElement }) => {...}` – Kitson Nov 27 '19 at 10:06
  • 1
    The custom interface destroys the rest of the information about the Event object. [@Kitson 's example](https://stackoverflow.com/questions/28900077/why-is-event-target-not-element-in-typescript#comment104373609_48443771) is additive and much better. – johnny Aug 04 '22 at 00:14
60

Typescript 3.2.4

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

e => console.log((e.target as Element).id)
dimpiax
  • 12,093
  • 5
  • 62
  • 45
  • Is that the same as the `event.target;` syntax? – Konrad Viltersten Aug 18 '19 at 20:13
  • @KonradViltersten, they do the same thing. The `as` syntax was introduced because it conflicted with JSX. It's recommended to use `as` for consistency. https://basarat.gitbooks.io/typescript/docs/types/type-assertion.html – Adam Sep 05 '19 at 15:57
  • 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). – Konrad Viltersten Sep 06 '19 at 08:12
  • @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. https://en.wikipedia.org/wiki/Anders_Hejlsberg – Bangonkali Feb 07 '22 at 14:40
  • 3
    Using `as` should be a last resort if you do not want a liar liar pants on fire codebase. – johnny Aug 04 '22 at 00:06
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 });
}
WickyNilliams
  • 5,218
  • 2
  • 31
  • 43
  • 1
    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). – Oleg Valter is with Ukraine Apr 13 '21 at 23:21
  • `type DOMEvent = E & { readonly target: T; };` Here's a variation on the above that doesn't clobber the original Event type. e.g. `DOMEvent` 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. – johnny Aug 04 '22 at 00:11
  • see also `React.MouseEvent` etc [here](https://stackoverflow.com/questions/42081549/typescript-react-event-types) and [here](https://stackoverflow.com/questions/44321326/property-value-does-not-exist-on-type-eventtarget-in-typescript) – milahu Oct 30 '22 at 17:24
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...
};
Maksim Kostromin
  • 3,273
  • 1
  • 32
  • 30
2

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];
  }
ciscoheat
  • 3,719
  • 1
  • 35
  • 52
  • 1
    I'm not sure about event types in Angular, but shouldn't your event be some kind of Event type instead of HTMLInputElement ? – Kaz Dec 16 '21 at 09:38
  • This technique only seems to work if you turn off strictTemplates in angularCompilerOptions in the tsconfig.json file. – billoreid Oct 18 '22 at 20:00
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;
}
hovado
  • 4,474
  • 1
  • 24
  • 30
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?

0

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;
    }
Gennady Magomaev
  • 1,157
  • 6
  • 8
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.

bersling
  • 17,851
  • 9
  • 60
  • 74
0

Answer from https://stackoverflow.com/a/48443771/5515861 and https://stackoverflow.com/a/58806863/5515861 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
    }
  }

abmap
  • 141
  • 5