TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
104: Typing DOM Elements and Events
I remember a junior dev on my team spending nearly two hours fighting with a simple search bar. They had written document.querySelector('.search-input') and then tried to read the .value property to send it to an API. TypeScript kept screaming that value didn't exist on type Element. Out of frustration, they almost used any just to make the red squiggly lines disappear. It’s a rite of passage, really—the moment you realize that TypeScript knows your HTML exists, but it has no idea what those elements actually are.
Telling TypeScript Exactly Which Element You're Grabbing
The problem is that querySelector is designed to be generic. It returns the type Element | null because it doesn't know if you're grabbing a <div>, a <span>, or a <input>. Since a generic Element doesn't have a .value property (only inputs and textareas do), TypeScript blocks you to prevent a runtime crash.
To fix this, we use Type Assertions. You're essentially telling the compiler, "Trust me, I know this specific element is an input." Here is how you do it correctly:
const searchInput = document.querySelector('.search-input') as HTMLInputElement;
const submitBtn = document.querySelector('#submit-btn') as HTMLButtonElement;
// Now TypeScript knows .value exists on searchInput
console.log(searchInput.value);
A quick heads-up: querySelector can return null if the element isn't found. In a real production app, I'd recommend a null check before using the element. But if you're 100% certain the element is hardcoded in your HTML, the as keyword is your best friend here.
Typing Your Event Handlers
When you move on to events, you'll notice another hurdle. If you write an inline arrow function for an event listener, TypeScript often defaults the event object e to any or a generic Event. A generic Event doesn't have properties like clientX (for mouse moves) or key (for keyboard presses).
You should use the specific event types provided by TypeScript. For a button click, that's MouseEvent. For a key press, it's KeyboardEvent. I usually prefer typing the event parameter directly in the function signature:
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
console.log('User pressed enter!');
}
};
searchInput.addEventListener('keyup', handleKeyUp);
The Target Casting Trap
Here is where most developers get tripped up: event.target. Even if you've typed the event as a MouseEvent, event.target is still typed as a generic EventTarget. This is because an event can bubble up from many different types of elements.
If you need to access a property specifically belonging to an input inside an event handler, you have to cast the target specifically. I've seen people try to cast the whole event, but that's not how it works. You cast the target:
const handleInput = (event: Event) => {
// We cast event.target to HTMLInputElement to access .value
const target = event.target as HTMLInputElement;
console.log('Current input value:', target.value);
};
searchInput.addEventListener('input', handleInput);
It feels a bit repetitive to keep casting, but it's the only way to maintain type safety when interacting with the loosely structured nature of the DOM.
📋 Practical Task
Exercise: Building a Typed Character Counter
Create a small TypeScript program that implements a character counter for a textarea. Your task is to:
- Select a
<textarea>and a<span>(for the count display) from the DOM using type assertions (as HTMLTextAreaElementandas HTMLSpanElement). - Create an event listener for the
'input'event on the textarea. - Inside the event handler, properly cast the
event.targetto access itsvalueproperty. - Update the
textContentof the span to show the current length of the text (e.g., "Characters: 12").
Goal: Ensure there are zero TypeScript compiler errors and avoid using the any type entirely.
There are no comments for now.