Домашнє завдання #20: Твій каталог на TypeScript
Фінал TypeScript-блоку. У ДЗ #14 ти побудував каталог своєї колекції на чистому JS — він працює, але одна одрукована назва поля, і все тихо ламається в рантаймі. Сьогодні ти зробиш те, що постійно роблять у реальних командах: рефакторинг робочого JS-проєкту на TypeScript. Функціональність та сама — але тепер компілятор стоїть на варті кожного поля.
Що ти вже маєш
- ДЗ #14 — робочий каталог: додавання, зміна статусу, видалення, редагування, фільтри, лічильник, localStorage. Це твій вихідний код
- ДЗ #19 — типи твоєї доменної моделі та generic-сховище. Сьогодні вони йдуть у бій
Приклади нижче — на дефолтній фільмотеці (елемент — фільм, статус — «переглянуто»). У тебе — твоя тема і твої назви.
Файлова структура
hw-20/
├── index.html ← з hw-14, онови шлях до скрипта на dist/app.js
├── styles.css ← з hw-14, без змін
├── tsconfig.json
├── package.json ← scripts: "build": "tsc", "dev": "tsc --watch"
└── src/
├── types.ts ← доменні типи (з hw-19) + типи стану
├── storage.ts ← generic TypedStorage для localStorage
├── dom.ts ← типізований доступ до елементів
└── app.ts ← логіка каталогу
Довідковий tsconfig.json (це конфіг, не рішення):
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"noEmitOnError": true
},
"include": ["src"]
}
Завдання
1. Типи (types.ts)
enum Filterдля фільтрів списку — три значення: всі / активні / завершені (назви — під твою тему: у фільмотеки «переглянуті», у подорожей «відвідані»)- Інтерфейс елемента колекції — візьми модель з ДЗ #19 і адаптуй:
readonly id: string, дати — як ISO-рядки, а неDate(localStorage зберігає лише текст —JSON.parseне поверне тобі об'єкт Date), статусне поле, optional-поля твоєї теми interface AppState— все, що описує поточний стан: масив елементів та активний фільтр
2. Generic-сховище (storage.ts)
У ДЗ #19 твій createStorage<T>() тримав дані в масиві. Тепер той самий принцип — але дані живуть у localStorage. Створи клас з такими сигнатурами:
class TypedStorage<T> {
constructor(key: string, defaultValue: T)
get(): T // прочитати; defaultValue, якщо пусто або зламаний JSON
set(value: T): void // серіалізувати та зберегти
update(updater: (current: T) => T): void // прочитати → перетворити → зберегти
clear(): void // видалити ключ
}
Використання: const itemsStorage = new TypedStorage<CollectionItem[]>("items", []) — і жодного «сирого» localStorage.getItem поза цим класом.
3. Типізований DOM (dom.ts)
- Функція
getElement<T extends HTMLElement>(selector: string): T— повертає елемент або кидає помилку з назвою селектора, якщо його немає. Більше ніяких мовчазнихnull - Всі елементи застосунку — з конкретними типами:
HTMLFormElement,HTMLInputElement,HTMLButtonElement,HTMLUListElement... ПростоHTMLElement— не зараховується
4. Логіка (app.ts)
Вся механіка твого каталогу з ДЗ #14 має працювати як раніше:
- Додавання елемента (кнопка + Enter, порожній ввід ігнорується)
- Перемикання статусу, видалення, редагування
- Фільтри (через
enum Filter), лічильник активних елементів - Збереження в localStorage через
TypedStorageпісля кожної зміни, відновлення при завантаженні
Технічні вимоги до подій:
- Обробники типізовані:
SubmitEvent,MouseEvent,KeyboardEvent— не голийEventтам, де відомо точніше - Делегування подій зі звуженням типу:
event.target— цеEventTarget | null, і компілятор змусить тебе чесно це обробити
5. Загальні вимоги типізації
strict: true, жодногоany— ні явного, ні прихованого- Всі функції — з типізованими параметрами та return types
npx tsc --noEmit— нуль помилок
Сценарії перевірки
- Додаєш елемент → він у списку, лічильник +1, у localStorage з'явився запис
- Перезавантажуєш сторінку → всі елементи та їхні статуси на місці
- Перемикаєш фільтр «завершені» → видно лише завершені, лічильник не змінюється (він рахує активні)
- Редагуєш елемент, тиснеш Escape → зміни скасовано
- Псуєш вручну JSON у localStorage → застосунок не падає, стартує з порожнім списком
Бонус
- Сортування списку (за датою / за назвою) через literal type
SortBy— з збереженням вибору - Експорт/імпорт колекції в JSON-файл з runtime-валідацією через type guard (
isCollectionItem) - Undo для видалення: тост «Елемент видалено — Повернути» на 5 секунд
- Drag-and-drop порядок елементів з типізацією
DragEvent
Підказки
Рівень 1: як підступитися до рефакторингу
Не переписуй з нуля — мігруй. Скопіюй script.js з hw-14 у src/app.ts, запусти npm run dev (tsc --watch) і сприймай список помилок компілятора як план роботи: кожна помилка — це місце, де JS дозволяв собі зайве. Винось типи в types.ts, роботу з localStorage — у storage.ts, пошук елементів — у dom.ts. Комить після кожного великого кроку.
Рівень 2: типові помилки міграції та інструменти
event.target має тип EventTarget | null — звужуй через instanceof HTMLElement або target.closest<HTMLLIElement>("[data-id]"). dataset.id — це string | undefined, перевір на undefined перед використанням. JSON.parse повертає any — саме тому весь парсинг живе всередині TypedStorage, а назовні виходить уже T. Для enum у розмітці зручно класти значення в data-filter і приводити: target.dataset.filter as Filter після перевірки.
Рівень 3: скелет міграції по кроках
types.ts: enum Filter, інтерфейс елемента (id: string, дати як ISO-рядки), AppStatestorage.ts: уget()— try/catch навколоJSON.parse; у catch повертайdefaultValue(це і є сценарій 5)dom.ts:const el = document.querySelector<T>(selector); if (!el) throw new Error(...); return el;app.ts: держи стан у змінній типуAppState; кожна операція = змінити стан →itemsStorage.set(...)→ перерендерити- Рендер: збирай елементи через
document.createElementабо template string; дата-атрибут з id — для делегування - Наприкінці — пройди всі 5 сценаріїв руками та зроби скріншот
Як здати
- Робота — у твоєму репозиторії курсу
my-web-course, у папціhw-20/ - У папці —
tsconfig.json,package.jsonтаREADME.md: 2-3 речення (тема каталогу, що змінилось порівняно з hw-14) + як зібрати й відкрити npx tsc --noEmitпроходить без помилок- Скріншот
hw-20/screenshots/desktop.png(~1440px): працюючий каталог з кількома елементами у різних статусах - Мінімум 3 коміти з осмисленими повідомленнями — наприклад: типи + tsconfig, storage + dom, міграція логіки
- Здай посиланням на папку
hw-20у репозиторії
Критерії оцінювання
| Критерій | Тип | Бали |
|---|---|---|
| types.ts: enum Filter, інтерфейс елемента на основі hw-19 (ISO-дати, readonly id), AppState | [код] | 15 |
| TypedStorage: всі 4 методи, generic, стійкість до зламаного JSON | [код] | 15 |
| dom.ts: getElement з помилкою при відсутності, конкретні типи елементів | [код] | 10 |
| CRUD працює: додавання, toggle статусу, видалення, редагування (сценарії 1, 4) | [поведінка] | 20 |
| Фільтри через enum та лічильник активних (сценарій 3) | [поведінка] | 10 |
| localStorage: збереження та відновлення після перезавантаження (сценарії 2, 5) | [поведінка] | 10 |
| Типізовані події та делегування зі звуженням event.target | [код] | 5 |
| strict: true, жодного any, npx tsc --noEmit без помилок | [код] | 5 |
| desktop.png: працюючий каталог з елементами в різних статусах | [скрін] | 5 |
| Якість коду | [код] | 5 |
| Разом | 100 | |
| Бонус: сортування / export-import з type guard / undo / drag-and-drop | [код] | +20 |
Що далі
Твій каталог тепер типобезпечний — і готовий до наступного стрибка: у Block 8 він переїде на React, а в ДЗ #25 отримає справжній бекенд з CRUD API на Next.js — з цими ж типами з ДЗ #19. Не видаляй цей код!