Skip to Content
Course content

107: Practice Exercise: Typing a Vanilla TS DOM Widget

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 querySelector to grab both elements, ensuring you use the correct specific types (HTMLTextAreaElement and HTMLSpanElement).
  • Implement a guard clause to ensure neither element is null before adding logic.
  • Add an 'input' event listener to the textarea.
  • Inside the listener, correctly type the event.target to access the current length of the text.
  • Update the innerText of the span to show the current count (e.g., "45/100").
Rating
0 0

There are no comments for now.

to be the first to leave a comment.