The error
Wire up an event listener, try to read event.target.value, and TypeScript immediately slaps you with:
input.addEventListener('change', (event) => {
console.log(event.target.value); // โ Property 'value' does not exist on type 'EventTarget'.
});
React hits the same wall:
const handleChange = (event: React.ChangeEvent) => {
console.log(event.target.value); // โ same error
};
Why this happens
event.target is typed as EventTarget โ the most generic DOM interface. It only guarantees a handful of methods: addEventListener, dispatchEvent, removeEventListener. Nothing about value, checked, files, or any element-specific property.
TypeScript won't narrow EventTarget to HTMLInputElement automatically. At the type level, an event can fire on any element โ a <div>, a <span>, a shadow DOM node. You have to tell the compiler what you're actually dealing with.
Fix 1 โ Type assertion (fastest fix)
Cast event.target to the element type you expect:
input.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
console.log(target.value); // โ
});
Match the interface to the element:
HTMLInputElementโ<input>HTMLTextAreaElementโ<textarea>HTMLSelectElementโ<select>HTMLButtonElementโ<button>
React makes this cleaner. Pass the element type as a generic parameter on the event type and skip the cast entirely:
// React โ generic parameter handles the typing
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value); // โ
no cast needed
};
<input type="text" onChange={handleChange} />
Fix 2 โ Use currentTarget instead of target
There's a subtler fix that avoids casting altogether. currentTarget is the element your listener is attached to. TypeScript infers its type from the element you called addEventListener on โ so you get precise typing for free.
const input = document.querySelector('input'); // HTMLInputElement | null
input?.addEventListener('change', (event) => {
// event.currentTarget is HTMLInputElement โ no cast needed
console.log(event.currentTarget?.value); // โ
});
Here's why the distinction matters: target is whatever the user actually clicked or typed into โ potentially a child element nested inside your container. currentTarget is always the element holding the listener. For form fields, currentTarget is almost always the right choice.
Fix 3 โ Type guard (safest for shared handlers)
One handler covering multiple element types? Use an instanceof guard. It narrows the type and protects you at runtime:
function handleEvent(event: Event) {
if (event.target instanceof HTMLInputElement) {
console.log(event.target.value); // โ
TypeScript knows the type here
} else if (event.target instanceof HTMLSelectElement) {
console.log(event.target.value); // โ
}
}
document.querySelector('form')?.addEventListener('change', handleEvent);
If the target turns out to be a button or some other unexpected element inside the form, the guard catches it cleanly instead of crashing at runtime.
Fix 4 โ Typed event handler helper (reusable pattern)
Writing the same cast in a dozen files gets old fast. Extract it once and reuse it everywhere:
function inputHandler(fn: (value: string, event: Event) => void) {
return (event: Event) => {
if (event.target instanceof HTMLInputElement) {
fn(event.target.value, event);
}
};
}
// Usage
document.getElementById('search')?.addEventListener(
'input',
inputHandler((value) => console.log('search:', value))
);
Verifying the fix
- The red squiggle in your editor disappears as soon as you apply the cast or generic parameter.
- Run
tsc --noEmitโ zero errors for that file. - Hover over
targetin your IDE. The tooltip should showHTMLInputElement(or whichever concrete type you declared), not the genericEventTarget. - Open DevTools in the browser and confirm the value logs correctly at runtime.
Which fix to use
- Quick one-off handler: type assertion (
as HTMLInputElement) - React forms: generic event type (
React.ChangeEvent<HTMLInputElement>) - Handler on a known element:
currentTargetโ no cast needed - Shared/polymorphic handler:
instanceofguard
Prevention
Turn on strict mode in tsconfig.json if you haven't already. It catches this whole class of errors early rather than letting them surface at runtime:
{
"compilerOptions": {
"strict": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"]
}
}
Double-check that "DOM" is in your lib array. Leave it out and TypeScript won't recognize HTMLInputElement at all โ you'll get a different, more confusing error that sends you down a rabbit hole.
One habit worth building: type your event handler parameters explicitly rather than leaning on inference from addEventListener. The tighter the type you declare upfront, the earlier the compiler catches mistakes โ before any of it reaches the browser.

