Common DOM Typing Mistakes in TypeScript

Learn the most common TypeScript mistakes when working with the DOM: forgetting null checks, using wrong event properties, and misusing type assertions.

7 min read

TypeScript catches many DOM mistakes at compile time, but the warnings only help if you read them. The most common errors happen when developers work around the type system instead of working with it. Each mistake below shows what goes wrong, why TypeScript flags it, and the correct fix.

Forgetting the null check

Every querySelector and getElementById call can return null. TypeScript includes null in the return type as a deliberate safety feature. Skipping the check with a non-null assertion hides a real problem.

typescripttypescript
// Wrong, assumes the element always exists
const button = document.querySelector<HTMLButtonElement>('#submit')!;
button.disabled = true;

If the element with id submit is not in the DOM, the non-null assertion tells TypeScript to trust you, but at runtime button is null and accessing .disabled throws a TypeError. The correct approach handles the null case explicitly.

typescripttypescript
// Correct, handles the missing element case
const button = document.querySelector<HTMLButtonElement>('#submit');
if (button === null) {
  console.error('Submit button is missing');
  return;
}
button.disabled = true;

The if check narrows the type from HTMLButtonElement or null to just HTMLButtonElement. After the check, TypeScript knows button is not null, and the code is safe at runtime.

Using event.target instead of event.currentTarget

event.target is typed as EventTarget or null because events can bubble from child elements. Accessing .value on event.target fails because EventTarget does not have that property.

typescripttypescript
// Wrong, event.target has no .value
nameInput.addEventListener('input', (event) => {
  console.log(event.target.value); // Compiler error
});

The compiler error says Property 'value' does not exist on type 'EventTarget'. Switching to event.currentTarget does not fix this by itself, because TypeScript types currentTarget as EventTarget or null too, even though you know it is the element the listener is attached to.

typescripttypescript
// Correct, read the outer variable through the closure
nameInput.addEventListener('input', () => {
  console.log(nameInput.value); // OK, nameInput is already HTMLInputElement
});

The listener already has access to nameInput through the closure, so there is no need to read the value off the event object at all. If you need the value from the event itself, assert the type instead: const input = event.currentTarget as HTMLInputElement. Either way, target is whatever element actually fired the event, which could be a child element with a different type than the one the listener is attached to.

Assuming fetch throws on HTTP errors

fetch only rejects on network failures, not on HTTP error status codes. A 404 or 500 response still gives you a valid Response object with ok set to false.

typescripttypescript
// Wrong, assumes fetch throws on bad status
async function getData() {
  const response = await fetch('/api/data');
  const data = await response.json(); // May parse error HTML as JSON
  return data;
}

If the server returns a 500 error page as HTML, response.json() tries to parse HTML as JSON and throws a SyntaxError with a confusing message. The correct pattern checks ok before parsing.

typescripttypescript
// Correct, checks status before parsing
async function getData() {
  const response = await fetch('/api/data');
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }
  const data = await response.json();
  return data;
}

This separates HTTP errors from JSON parsing errors. The error message includes the actual status code, making debugging much easier than a cryptic JSON parse failure.

Misusing type assertions

Type assertions like as HTMLInputElement tell TypeScript to treat a value as a specific type without any runtime check. Overusing them hides real type mismatches.

typescripttypescript
// Wrong, assertion hides the null possibility
const input = document.querySelector('#name') as HTMLInputElement;
input.value = 'hello'; // Crashes if element is missing

The assertion bypasses both the null check and the element type check. If the selector matches a div instead of an input, or nothing at all, the code crashes at runtime with no compile-time warning. Prefer a generic querySelector call with a null check.

typescripttypescript
// Correct, generic with null check
const input = document.querySelector<HTMLInputElement>('#name');
if (input === null) throw new Error('Name input not found');
input.value = 'hello'; // Safe

The generic tells TypeScript the expected element type, and the null check handles the missing element case. This is safer than an assertion and just as concise.

Using the wrong value property

HTMLInputElement has multiple value-related properties, and using the wrong one gives you the wrong type or wrong data. The most common mix-up is using .value on a checkbox.

typescripttypescript
// Wrong, .value on a checkbox is not the checked state
const checkbox = document.querySelector<HTMLInputElement>('#agree');
if (checkbox) {
  if (checkbox.value) {  // Always truthy if set, even when unchecked
    // This does not mean the box is checked
  }
}

Checkbox .value is the string sent on form submission, not whether the box is ticked. Use .checked for the boolean state.

typescripttypescript
// Correct, .checked is the boolean state
if (checkbox.checked) {
  console.log('User agreed');
}

The same principle applies to number and date inputs. Use .valueAsNumber and .valueAsDate instead of parsing .value yourself. The dedicated properties give you the correct type and handle edge cases like empty inputs consistently.

Ignoring that types are compile-time only

TypeScript types disappear at runtime. An interface or type annotation does not validate the data that comes from the DOM, an API, or localStorage.

typescripttypescript
// Wrong, assumes the type guarantees runtime shape
interface UserData {
  name: string;
  role: string;
}
 
const stored = localStorage.getItem('user');
const user: UserData = JSON.parse(stored); // stored could be null

If stored is null, JSON.parse throws. If the stored JSON is missing the role property, TypeScript will not warn you because the type annotation is only checked at compile time. Always validate at runtime.

typescripttypescript
// Correct, runtime checks before using the data
const stored = localStorage.getItem('user');
if (stored === null) return null;
 
try {
  const parsed = JSON.parse(stored);
  if (typeof parsed.name === 'string' && typeof parsed.role === 'string') {
    const user: UserData = parsed; // Safe after runtime check
    return user;
  }
} catch {
  localStorage.removeItem('user');
}
return null;

The runtime checks confirm that parsed.name and parsed.role are strings before the code treats the object as UserData. The try-catch handles invalid JSON. This is more code, but it is correct code that does not crash on unexpected data.

See Type DOM Elements in TypeScript for element type fundamentals. See Type Fetch in the Browser with TypeScript for fetch-specific error handling patterns.

Rune AI

Rune AI

Key Insights

  • Always check for null after querySelector, getElementById, and similar DOM lookups.
  • event.target and event.currentTarget are both typed as EventTarget or null, so read the element through a closure variable or assert the type before accessing properties.
  • Check response.ok after fetch because it does not throw on HTTP errors like 404.
  • Prefer instanceof narrowing over type assertions for safer runtime type checks.
  • Read values with the correct property: .value for text, .checked for checkboxes, .valueAsNumber for numbers.
RunePowered by Rune AI

Frequently Asked Questions

What is the most common DOM typing mistake in TypeScript?

Forgetting to handle null after querySelector or getElementById. These methods can return null when the element is not in the DOM, and TypeScript enforces the check. Skipping it with a non-null assertion hides a real runtime error.

Why does event.target.value cause a TypeScript error?

event.target is typed as EventTarget or null, and EventTarget does not have a .value property. event.currentTarget has the same EventTarget or null type, so read the value from the outer variable through the closure instead, or assert the element type when you truly need it from the event object.

Should I use type assertions to silence DOM type errors?

Only when you are certain the element type is correct and you have already done a null check. Type assertions bypass the compiler's safety checks. Prefer instanceof narrowing or generic querySelector calls instead of sprinkling as HTMLInputElement throughout your code.

Conclusion

The most common DOM typing mistakes all stem from ignoring the possibility of null, reading element properties off the event object without narrowing, assuming fetch throws on HTTP errors, and reaching for type assertions too quickly. Handle null explicitly, prefer reading typed variables through closures over event.target or event.currentTarget, check response.ok before parsing, and prefer narrowing over assertions.