-
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
107: Practice Exercise: Typing a Vanilla TS DOM Widget
Think of the DOM like a massive warehouse full of crates. If you tell a warehouse worker to "go grab a crate," they'll bring you something, but you have no idea what's inside. You can't assume it's a box of screws if it might actually be a refrigerator. To actually use the contents, you have to verify exactly what kind of crate it is before you try to open it. If you try to use a screwdriver on a refrigerator, you're just wasting your time (or breaking something).
In TypeScript, document.querySelector is that warehouse worker. By default, it returns a generic Element. The problem is that a generic Element doesn't have a .value property or a .checked property. Those only exist on specific "crates" like HTMLInputElement or HTMLTextAreaElement. If you try to access .value on a generic Element, TypeScript will yell at you because it's trying to protect you from the "refrigerator" scenario.
Stop treating every element like a generic box
When I'm building a small widget—say, a custom search bar or a toggle switch—I don't want to spend my whole day fighting the compiler. The trick is to be explicit about what you're grabbing the moment you grab it. I usually do this using a type assertion (the as keyword) because, as the developer, I'm the one who wrote the HTML; I know that #search-input is an input field.
// This will fail because 'Element' doesn't have '.value'
const input = document.querySelector('#search-input');
console.log(input.value);
// This is how we tell TS: "Trust me, I know this is an input"
const searchInput = document.querySelector('#search-input') as HTMLInputElement;
console.log(searchInput.value); // Now we're talking.
The "Maybe" of the Document Object Model
Here is where things get annoying: querySelector can return null if the element isn't on the page. I've seen plenty of juniors just slap a non-null assertion (the !) on everything to make the red lines go away. Please, don't do that. It's a habit that leads to the dreaded Cannot read property 'addEventListener' of null at runtime.
Instead, I prefer a quick "guard clause." It keeps the logic clean and ensures that the rest of my widget code only runs if the DOM is actually ready and the elements exist. It's a small price to pay for a crash-proof UI.
const submitBtn = document.querySelector('#submit-btn') as HTMLButtonElement | null;
if (!submitBtn) {
throw new Error("Critical UI element #submit-btn is missing from the DOM.");
}
// From this point down, TS knows submitBtn is definitely an HTMLButtonElement
submitBtn.addEventListener('click', () => {
console.log('Clicked!');
});
Mapping types to event targets
One of the most common places where typing breaks down in a widget is inside the event listener. If you're using a shared handler function, event.target is typed as a generic EventTarget. Just like our warehouse crates, a generic target doesn't have a .value.
When I'm handling an input event, I cast the target inside the function. It feels repetitive, but it's the only way to get full autocomplete and type safety for the properties that actually matter for that specific widget's behavior.
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
console.log(`User typed: ${target.value}`);
};📋 Practical Task
Exercise: Building a Typed Character Counter Widget
Your task is to create a small "Character Counter" widget. This widget should track the length of a text area and update a label in real-time. If the character count exceeds 100, the label should turn red (or you can just log a warning to the console).
Requirements:
- Create a TypeScript file that targets the following HTML structure:
<textarea id="bio-input"></textarea> <span id="char-count">0/100</span> - Use
querySelectorto grab both elements, ensuring you use the correct specific types (HTMLTextAreaElementandHTMLSpanElement). - Implement a guard clause to ensure neither element is
nullbefore adding logic. - Add an
'input'event listener to the textarea. - Inside the listener, correctly type the
event.targetto access the current length of the text. - Update the
innerTextof the span to show the current count (e.g., "45/100").
There are no comments for now.