Skip to main content

TypeScript DOM Manipulation

Introduction

The Document Object Model (DOM) is a programming interface for web documents, representing the page so that programs can change the document structure, style, and content. TypeScript, with its static typing system, provides an enhanced way to work with the DOM compared to vanilla JavaScript, offering better tooling, type checking, and developer experience.

In this tutorial, we'll explore how to leverage TypeScript's features to manipulate the DOM effectively and safely. We'll cover selecting elements, modifying content, handling events, creating elements dynamically, and managing common pitfalls.

TypeScript and DOM Basics

Setting Up TypeScript for DOM Manipulation

Before we dive into DOM manipulation, let's ensure our TypeScript configuration is properly set up:

typescript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2015",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"strict": true
}
}

Including the DOM and DOM.Iterable libraries in the configuration gives TypeScript access to all the DOM type definitions.

Understanding DOM Types in TypeScript

TypeScript provides built-in types for DOM elements and operations:

typescript
// Common DOM types
const element: HTMLElement = document.getElementById("app")!;
const button: HTMLButtonElement = document.querySelector("button")!;
const inputs: NodeListOf<HTMLInputElement> = document.querySelectorAll("input");

The exclamation mark (!) is the non-null assertion operator, telling TypeScript that we're certain the element exists. However, a safer approach is to check for null:

typescript
const element = document.getElementById("app");
if (element) {
// Safely use element here
}

Selecting DOM Elements with TypeScript

Basic Element Selection

TypeScript provides proper type information for DOM selection methods:

typescript
// Type-safe DOM selection
const app: HTMLElement | null = document.getElementById("app");
const buttons: NodeListOf<Element> = document.querySelectorAll(".button");
const firstButton: Element | null = document.querySelector(".button");
const formElements: HTMLCollectionOf<HTMLFormElement> = document.forms;

Type Assertion for More Specific Element Types

Sometimes you need to tell TypeScript about a more specific element type:

typescript
// Using type assertion for specific element types
const loginButton = document.querySelector("#login") as HTMLButtonElement;
const usernameInput = <HTMLInputElement>document.querySelector("#username");

Both forms (as HTMLButtonElement and <HTMLInputElement>) are valid type assertions, though the as syntax is generally preferred.

Modifying DOM Elements

Changing Element Content

TypeScript provides type checking for DOM properties:

typescript
// Changing text content
const header = document.getElementById("header") as HTMLHeadingElement;
if (header) {
header.textContent = "Welcome to TypeScript DOM Manipulation";
header.innerHTML = "Welcome to <strong>TypeScript</strong> DOM Manipulation";
}

Manipulating Element Attributes

typescript
// Working with attributes
const image = document.querySelector("img") as HTMLImageElement;
if (image) {
image.src = "typescript-logo.png";
image.alt = "TypeScript Logo";

// Setting custom attributes
image.setAttribute("data-loaded", "true");

// Getting attribute values
const imageSource: string | null = image.getAttribute("src");
}

Modifying Styles

typescript
// Changing styles
const container = document.getElementById("container");
if (container) {
// Direct style manipulation
container.style.backgroundColor = "#f0f0f0";
container.style.padding = "20px";
container.style.borderRadius = "8px";

// Using CSS classes
container.classList.add("active");
container.classList.remove("hidden");
container.classList.toggle("expanded");

// Checking if a class exists
const isActive: boolean = container.classList.contains("active");
}

Event Handling in TypeScript

Basic Event Handling

typescript
// Type-safe event handling
const button = document.querySelector("#submitButton") as HTMLButtonElement;

button.addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
console.log("Button clicked!");
});

Creating Typed Event Handlers

typescript
// Creating typed event handlers
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
console.log(`Input changed: ${target.value}`);
}

// Using the handler
const nameInput = document.getElementById("name") as HTMLInputElement;
nameInput.addEventListener("input", handleInput);

Event Delegation with TypeScript

typescript
// Event delegation with proper typing
const todoList = document.querySelector(".todo-list") as HTMLUListElement;

todoList.addEventListener("click", (event: MouseEvent) => {
const target = event.target as HTMLElement;

if (target.matches(".delete-button")) {
const todoItem = target.closest(".todo-item") as HTMLLIElement;
todoItem.remove();
}

if (target.matches(".toggle-button")) {
const todoItem = target.closest(".todo-item") as HTMLLIElement;
todoItem.classList.toggle("completed");
}
});

Creating and Manipulating Elements

Creating DOM Elements

typescript
// Creating elements with TypeScript
const createTodoItem = (text: string): HTMLLIElement => {
const li = document.createElement("li");
li.className = "todo-item";

const span = document.createElement("span");
span.textContent = text;

const deleteButton = document.createElement("button");
deleteButton.textContent = "Delete";
deleteButton.className = "delete-button";

li.appendChild(span);
li.appendChild(deleteButton);

return li;
};

// Using the function
const todoList = document.querySelector(".todo-list") as HTMLUListElement;
const newTodo = createTodoItem("Learn TypeScript DOM");
todoList.appendChild(newTodo);

Working with Document Fragments

typescript
// Using DocumentFragment for efficient DOM updates
const addMultipleTodos = (todos: string[]): void => {
const todoList = document.querySelector(".todo-list") as HTMLUListElement;
const fragment = document.createDocumentFragment();

todos.forEach(todo => {
const todoItem = createTodoItem(todo);
fragment.appendChild(todoItem);
});

todoList.appendChild(fragment);
};

// Using the function
addMultipleTodos([
"Learn TypeScript Basics",
"Master DOM Manipulation",
"Build a TypeScript Project"
]);

Advanced DOM Manipulation Techniques

Custom Elements with TypeScript

typescript
// Defining a custom element class with TypeScript
class TodoCounter extends HTMLElement {
private count: number = 0;

constructor() {
super();
this.attachShadow({ mode: 'open' });
}

connectedCallback() {
this.render();
this.addEventListener('click', this.increment);
}

disconnectedCallback() {
this.removeEventListener('click', this.increment);
}

private increment = () => {
this.count++;
this.render();
}

private render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
padding: 10px;
background-color: #eee;
border-radius: 4px;
cursor: pointer;
}
</style>
<span>Todo Count: ${this.count}</span>
`;
}
}
}

// Register the custom element
customElements.define('todo-counter', TodoCounter);

// Using the custom element
const counter = document.createElement('todo-counter') as TodoCounter;
document.body.appendChild(counter);

Using Intersection Observer API

typescript
// Setting up an intersection observer with TypeScript
interface LazyImageElement extends HTMLImageElement {
dataset: DOMStringMap & {
src: string;
};
}

const setupLazyLoading = (): void => {
const lazyImages = Array.from(
document.querySelectorAll('img[data-src]')
) as LazyImageElement[];

const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as LazyImageElement;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});

lazyImages.forEach(img => imageObserver.observe(img));
};

// Initialize lazy loading
document.addEventListener('DOMContentLoaded', setupLazyLoading);

Real-world Example: Todo Application

Let's build a simple todo application using TypeScript for DOM manipulation:

typescript
// Define Todo interface
interface Todo {
id: number;
text: string;
completed: boolean;
}

// Todo application class
class TodoApp {
private todos: Todo[] = [];
private nextId: number = 1;
private todoList: HTMLUListElement;
private todoInput: HTMLInputElement;

constructor() {
// Select elements
this.todoList = document.getElementById('todo-list') as HTMLUListElement;
this.todoInput = document.getElementById('todo-input') as HTMLInputElement;

// Setup event listeners
const form = document.getElementById('todo-form') as HTMLFormElement;
form.addEventListener('submit', this.handleFormSubmit);

this.todoList.addEventListener('click', this.handleTodoClick);
}

private handleFormSubmit = (event: Event): void => {
event.preventDefault();
const text = this.todoInput.value.trim();

if (text) {
this.addTodo(text);
this.todoInput.value = '';
}
}

private handleTodoClick = (event: MouseEvent): void => {
const target = event.target as HTMLElement;

if (target.classList.contains('todo-delete')) {
const todoId = Number(target.dataset.id);
this.deleteTodo(todoId);
} else if (target.classList.contains('todo-toggle')) {
const todoId = Number(target.dataset.id);
this.toggleTodo(todoId);
}
}

private addTodo(text: string): void {
const todo: Todo = {
id: this.nextId++,
text,
completed: false
};

this.todos.push(todo);
this.renderTodo(todo);
}

private renderTodo(todo: Todo): void {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.dataset.id = String(todo.id);

li.innerHTML = `
<input type="checkbox" class="todo-toggle" data-id="${todo.id}"
${todo.completed ? 'checked' : ''}>
<span class="todo-text">${todo.text}</span>
<button class="todo-delete" data-id="${todo.id}">Delete</button>
`;

this.todoList.appendChild(li);
}

private deleteTodo(id: number): void {
this.todos = this.todos.filter(todo => todo.id !== id);
const todoElement = this.todoList.querySelector(`[data-id="${id}"]`);
if (todoElement) {
todoElement.remove();
}
}

private toggleTodo(id: number): void {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
const todoElement = this.todoList.querySelector(`[data-id="${id}"]`);
if (todoElement) {
todoElement.classList.toggle('completed');
}
}
}

public render(): void {
this.todoList.innerHTML = '';
this.todos.forEach(todo => this.renderTodo(todo));
}
}

// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const app = new TodoApp();
});

To use this Todo application, you would need the following HTML:

html
<div class="todo-container">
<h2>TypeScript Todo App</h2>

<form id="todo-form">
<input type="text" id="todo-input" placeholder="Add a new todo...">
<button type="submit">Add</button>
</form>

<ul id="todo-list" class="todo-list"></ul>
</div>

And some CSS to style it:

css
.todo-item {
display: flex;
align-items: center;
padding: 8px;
margin-bottom: 8px;
background-color: #f9f9f9;
border-radius: 4px;
}

.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}

.todo-text {
flex-grow: 1;
margin: 0 10px;
}

.todo-delete {
background-color: #ff4d4d;
color: white;
border: none;
border-radius: 3px;
padding: 3px 8px;
cursor: pointer;
}

Common Pitfalls and Best Practices

Avoiding Type Errors

typescript
// ❌ Unsafe approach - might cause runtime errors
const button = document.getElementById("submitButton");
button.addEventListener("click", () => {}); // Error: button might be null

// ✅ Safe approach with type checking
const button = document.getElementById("submitButton");
if (button) {
button.addEventListener("click", () => {});
}

// ✅ Alternative with type assertion (use when you're certain)
const button = document.getElementById("submitButton") as HTMLButtonElement;
button.addEventListener("click", () => {});

Working with Forms and Input Values

typescript
// ❌ Unsafe approach to form handling
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
e.preventDefault();
const nameInput = document.querySelector("#name");
console.log(nameInput.value); // Error: value doesn't exist on Element
});

// ✅ Type-safe form handling
const form = document.querySelector("form");
if (form) {
form.addEventListener("submit", (e: SubmitEvent) => {
e.preventDefault();
const nameInput = document.querySelector("#name") as HTMLInputElement;
if (nameInput) {
console.log(nameInput.value); // Safe: TypeScript knows it's an input
}
});
}

Performance Considerations

typescript
// ❌ Inefficient DOM manipulation
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
document.body.appendChild(div); // Causes reflow each time
}

// ✅ Efficient batch DOM manipulation
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // Single reflow

Summary

TypeScript provides significant advantages when working with the DOM:

  1. Type safety: TypeScript helps catch errors before runtime, reducing bugs in your DOM manipulation code.
  2. Better editor support: Get autocompletion and documentation for DOM methods and properties.
  3. Improved readability: Type declarations make code intentions clearer and easier to understand.
  4. Refactoring confidence: Change your code with confidence knowing TypeScript will catch breaking changes.

We've covered:

  • Setting up TypeScript for DOM manipulation
  • Selecting and type-checking DOM elements
  • Modifying element content, attributes, and styles
  • Handling events with proper typing
  • Creating and manipulating DOM elements
  • Building a real-world todo application
  • Best practices and common pitfalls

By leveraging TypeScript's type system, you can create more robust and maintainable web applications while catching potential DOM-related errors at development time rather than at runtime.

Exercises

  1. Create a simple form validator that uses TypeScript to check input values and display error messages.
  2. Build a custom image carousel component using TypeScript classes and DOM manipulation.
  3. Implement a drag-and-drop interface for reordering list items with TypeScript.
  4. Create a TypeScript utility library for common DOM operations with proper typing.
  5. Convert an existing JavaScript application to use TypeScript for DOM manipulation.

Additional Resources

Learning to manipulate the DOM with TypeScript is a valuable skill that will help you build safer, more maintainable web applications. As you practice, you'll discover the full power of combining TypeScript's static typing with dynamic DOM manipulation.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)