TypeScript з DOM
TypeScript з DOM — охоронець перевіряє бейджі на вході в будівлю з лупою
У Block 5 ми активно працювали з DOM: шукали елементи, додавали обробники подій, маніпулювали класами та атрибутами. Тепер навчимось робити це type-safe -- з повною типізацією. TypeScript має вбудовані типи для всіх DOM-елементів, подій та API браузера.
HTML Element Types
TypeScript знає про всі HTML-елементи та їхні типи:
// querySelector повертає Element | null
const element = document.querySelector(".container");
// element: Element | null
// Щоб отримати конкретний тип елемента:
const input = document.querySelector("input");
// input: HTMLInputElement | null
const button = document.querySelector("button");
// button: HTMLButtonElement | null
const link = document.querySelector("a");
// link: HTMLAnchorElement | null
Основні типи елементів
| HTML тег | TypeScript тип |
|---|---|
<div>, <span>, <p> | HTMLDivElement, HTMLSpanElement, HTMLParagraphElement |
<input> | HTMLInputElement |
<button> | HTMLButtonElement |
<a> | HTMLAnchorElement |
<form> | HTMLFormElement |
<img> | HTMLImageElement |
<select> | HTMLSelectElement |
<textarea> | HTMLTextAreaElement |
<canvas> | HTMLCanvasElement |
| Будь-який елемент | HTMLElement |
Проблема з null
querySelector завжди повертає Element | null, бо елемент може не існувати на сторінці. TypeScript змушує тебе обробити цей випадок:
const title = document.querySelector("h1");
// title: HTMLHeadingElement | null
// console.log(title.textContent); // Помилка: 'title' is possibly 'null'
// Рішення 1: перевірка на null (рекомендовано)
if (title) {
console.log(title.textContent); // OK
}
// Рішення 2: optional chaining
console.log(title?.textContent); // string | undefined
// Рішення 3: early return
function updateTitle(text: string): void {
const title = document.querySelector("h1");
if (!title) return; // early return якщо немає елемента
title.textContent = text; // TypeScript знає: title не null
}
Type Assertions -- твердження про тип
Type assertion каже TypeScript: "Я знаю тип краще за тебе":
// querySelector з класом повертає Element | null
const input = document.querySelector(".email-input");
// input: Element | null
// Type assertion -- ми знаємо, що це HTMLInputElement
const emailInput = document.querySelector(".email-input") as HTMLInputElement;
// emailInput: HTMLInputElement
// Тепер маємо доступ до .value
console.log(emailInput.value);
Type assertion не перевіряє тип під час виконання! Якщо елемент насправді не HTMLInputElement, TypeScript не покаже помилку -- помилка буде в рантаймі. Використовуй assertions тільки коли впевнений у типі.
Assertion з перевіркою (безпечніший варіант)
const element = document.querySelector(".email-input");
if (element instanceof HTMLInputElement) {
// TypeScript автоматично звузив тип -- assertion не потрібен!
console.log(element.value);
}
Non-null Assertion (!)
Оператор ! каже TypeScript "це точно не null/undefined":
// Ми знаємо, що елемент існує (наприклад, він завжди є в HTML)
const app = document.getElementById("app")!;
// app: HTMLElement (без null)
// Без ! було б: HTMLElement | null
Використовуй ! дуже обережно. Якщо елемент все ж виявиться null, отримаєш runtime error. Краще використовувати if перевірку або optional chaining.
Type Guards -- захисники типів
Вбудовані type guards
Ми вже використовували typeof, instanceof та in для звуження типів (урок 7.2). З DOM вони особливо корисні:
function handleElement(element: HTMLElement): void {
if (element instanceof HTMLInputElement) {
console.log("Input value:", element.value);
} else if (element instanceof HTMLAnchorElement) {
console.log("Link href:", element.href);
} else if (element instanceof HTMLImageElement) {
console.log("Image src:", element.src);
} else {
console.log("Text:", element.textContent);
}
}
Custom Type Guards -- користувацькі type guards
Можна створити свою функцію-guard з предикатом is:
interface User {
type: "user";
name: string;
email: string;
}
interface Admin {
type: "admin";
name: string;
email: string;
permissions: string[];
}
type Person = User | Admin;
// Custom type guard -- зверни увагу на return type: person is Admin
function isAdmin(person: Person): person is Admin {
return person.type === "admin";
}
function greet(person: Person): string {
if (isAdmin(person)) {
// TypeScript знає: person -- це Admin
return `Привіт, адмін ${person.name}! Дозволи: ${person.permissions.join(", ")}`;
}
// TypeScript знає: person -- це User
return `Привіт, ${person.name}!`;
}
Type guard для DOM
function isInputElement(element: Element): element is HTMLInputElement {
return element.tagName === "INPUT";
}
function isFormElement(element: Element): element is HTMLFormElement {
return element.tagName === "FORM";
}
// Використання
const elements = document.querySelectorAll("*");
elements.forEach((el) => {
if (isInputElement(el)) {
console.log("Input:", el.value); // el: HTMLInputElement
}
});
Типізація подій
TypeScript знає типи для всіх DOM-подій:
// Click event
const button = document.querySelector("button");
if (button) {
button.addEventListener("click", (event: MouseEvent) => {
console.log(`Клік на координатах: ${event.clientX}, ${event.clientY}`);
// event.target: EventTarget | null
});
}
// Input event
const input = document.querySelector("input");
if (input) {
input.addEventListener("input", (event: Event) => {
const target = event.target as HTMLInputElement;
console.log("Значення:", target.value);
});
}
// Keyboard event
document.addEventListener("keydown", (event: KeyboardEvent) => {
console.log(`Натиснуто: ${event.key}`);
if (event.key === "Escape") {
// закрити модалку
}
});
// Submit event
const form = document.querySelector("form");
if (form) {
form.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
console.log("Name:", formData.get("name"));
});
}
Основні типи подій
| Подія | TypeScript тип |
|---|---|
| click, dblclick, mousedown, mouseup | MouseEvent |
| keydown, keyup, keypress | KeyboardEvent |
| input, change | Event |
| submit | SubmitEvent |
| focus, blur | FocusEvent |
| drag, drop | DragEvent |
| scroll, resize | Event |
Типізація Fetch API
Один з найважливіших сценаріїв -- типізація даних з API:
// Інтерфейс для даних з API
interface ApiUser {
id: number;
name: string;
email: string;
phone: string;
}
// Простий fetch з типізацією
async function fetchUser(id: number): Promise<ApiUser> {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data: ApiUser = await response.json();
return data;
}
// Використання
async function main(): Promise<void> {
const user = await fetchUser(1);
console.log(user.name); // TypeScript знає -- це string
console.log(user.email); // string
}
Generic fetch helper
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json() as Promise<T>;
}
// Використання -- тип передається як generic
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
const post = await fetchJson<Post>(
"https://jsonplaceholder.typicode.com/posts/1"
);
// post: Post
const posts = await fetchJson<Post[]>(
"https://jsonplaceholder.typicode.com/posts"
);
// posts: Post[]
response.json() повертає Promise<any>. Type assertion (as Promise<T>) не перевіряє дані під час виконання -- якщо API поверне інший формат, TypeScript не допоможе. У реальних проєктах використовують бібліотеки валідації (Zod, io-ts) для runtime-перевірки.
Типізація POST-запиту
interface CreatePostData {
title: string;
body: string;
userId: number;
}
interface CreatePostResponse {
id: number;
title: string;
body: string;
userId: number;
}
async function createPost(data: CreatePostData): Promise<CreatePostResponse> {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json() as Promise<CreatePostResponse>;
}
// Використання
const newPost = await createPost({
title: "Мій пост",
body: "Зміст поста",
userId: 1,
});
console.log(`Створено пост #${newPost.id}`);
Практика: типізований Todo-додаток (фрагмент)
// Типи
interface Todo {
id: number;
text: string;
completed: boolean;
createdAt: Date;
}
type TodoFilter = "all" | "active" | "completed";
// DOM-елементи з типами
function initApp(): void {
const form = document.querySelector<HTMLFormElement>("#todo-form");
const input = document.querySelector<HTMLInputElement>("#todo-input");
const list = document.querySelector<HTMLUListElement>("#todo-list");
const filterBtns = document.querySelectorAll<HTMLButtonElement>(".filter-btn");
if (!form || !input || !list) {
throw new Error("Required DOM elements not found");
}
let todos: Todo[] = [];
let currentFilter: TodoFilter = "all";
// Type-safe event handler
form.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
const newTodo: Todo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date(),
};
todos.push(newTodo);
input.value = "";
renderTodos();
});
// Фільтрація
function getFilteredTodos(): Todo[] {
switch (currentFilter) {
case "active":
return todos.filter((t) => !t.completed);
case "completed":
return todos.filter((t) => t.completed);
default:
return todos;
}
}
function renderTodos(): void {
const filtered = getFilteredTodos();
list!.innerHTML = filtered
.map(
(todo) => `
<li data-id="${todo.id}" class="${todo.completed ? "completed" : ""}">
${todo.text}
</li>
`
)
.join("");
}
}
initApp();
Зверни увагу на document.querySelector<HTMLFormElement>(...) -- це generic-виклик querySelector, який одразу задає правильний тип повернення. Не потрібен as assertion!
Підсумок
- TypeScript має типи для всіх DOM-елементів:
HTMLInputElement,HTMLButtonElementтощо querySelectorповертаєElement | null-- завжди перевіряй наnull- Type assertions (
as) -- ми кажемо TypeScript, який тип очікуємо (без runtime-перевірки) - Non-null assertion (
!) -- ми впевнені, що значення неnull(обережно!) - Custom type guards (
is) -- функції для звуження типу - DOM події типізовані:
MouseEvent,KeyboardEvent,SubmitEvent - Fetch типізується через generics або assertions
querySelector<T>()-- generic версія, яка повертає правильний тип
Що далі?
Блок TypeScript завершено! Ти тепер знаєш основи типізації: базові типи, union/literal types, інтерфейси, generics, utility types та роботу з DOM. У наступному блоці -- Block 8: React -- ти будеш писати React-компоненти з TypeScript. Всі ці знання стануть фундаментом для створення типізованих компонентів, хуків та API-запитів.
Корисні посилання:
- TypeScript Handbook: DOM Manipulation -- офіційний гайд по DOM
- TypeScript: lib.dom.d.ts -- повний список DOM-типів у TypeScript
- Zod -- бібліотека для runtime-валідації типів (ідеальний компаньйон для fetch)
- Total TypeScript: Type Guards -- поглиблений матеріал по type guards