Todo List на TypeScript
Переписати Todo List з Block 5 (ДЗ #14) на TypeScript з повною типізацією: інтерфейси, typed DOM, generic localStorage helper, enum для фільтрів та type-safe event handlers.
Завдання
1. Типи та інтерфейси
Створи файл types.ts з наступними типами:
// Пріоритет завдання
enum Priority {
Low = "low",
Medium = "medium",
High = "high",
}
// Фільтр відображення
enum Filter {
All = "all",
Active = "active",
Completed = "completed",
}
// Сортування
type SortBy = "date" | "priority" | "alphabetical";
type SortOrder = "asc" | "desc";
// Основний інтерфейс Todo
interface Todo {
readonly id: string;
text: string;
completed: boolean;
priority: Priority;
createdAt: string; // ISO string для серіалізації в localStorage
completedAt?: string;
}
// Стан додатку
interface AppState {
todos: Todo[];
filter: Filter;
sortBy: SortBy;
sortOrder: SortOrder;
searchQuery: string;
}
2. Generic Storage Helper
Створи файл storage.ts з generic helper для localStorage:
// Generic helper для роботи з localStorage
class TypedStorage<T> {
constructor(private key: string, private defaultValue: T) {}
get(): T {
// Прочитати з localStorage, повернути defaultValue якщо немає
}
set(value: T): void {
// Зберегти в localStorage
}
update(updater: (current: T) => T): void {
// Прочитати, застосувати функцію, зберегти
}
clear(): void {
// Видалити з localStorage
}
}
Використання:
const todoStorage = new TypedStorage<Todo[]>("todos", []);
const settingsStorage = new TypedStorage<Omit<AppState, "todos">>("settings", {
filter: Filter.All,
sortBy: "date",
sortOrder: "desc",
searchQuery: "",
});
3. DOM Elements з типами
Створи файл dom.ts або секцію з типізованими DOM-елементами:
// Типізований доступ до DOM елементів
function getElement<T extends HTMLElement>(selector: string): T {
const element = document.querySelector<T>(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
return element;
}
// Елементи додатку
const elements = {
form: getElement<HTMLFormElement>("#todo-form"),
input: getElement<HTMLInputElement>("#todo-input"),
prioritySelect: getElement<HTMLSelectElement>("#priority-select"),
list: getElement<HTMLUListElement>("#todo-list"),
filterButtons: document.querySelectorAll<HTMLButtonElement>(".filter-btn"),
searchInput: getElement<HTMLInputElement>("#search-input"),
counter: getElement<HTMLSpanElement>("#todo-counter"),
clearCompleted: getElement<HTMLButtonElement>("#clear-completed"),
sortSelect: getElement<HTMLSelectElement>("#sort-select"),
};
4. Основна логіка
Створи файл app.ts з класом або набором функцій:
class TodoApp {
private state: AppState;
private todoStorage: TypedStorage<Todo[]>;
private settingsStorage: TypedStorage<Omit<AppState, "todos">>;
constructor() {
// Ініціалізація storage та стану
}
// CRUD операції
addTodo(text: string, priority: Priority): Todo { /* ... */ }
removeTodo(id: string): void { /* ... */ }
toggleTodo(id: string): void { /* ... */ }
editTodo(id: string, newText: string): void { /* ... */ }
// Фільтрація та сортування
setFilter(filter: Filter): void { /* ... */ }
setSortBy(sortBy: SortBy): void { /* ... */ }
setSearchQuery(query: string): void { /* ... */ }
// Отримати відфільтровані та відсортовані todos
getVisibleTodos(): Todo[] { /* ... */ }
// Статистика
getStats(): { total: number; active: number; completed: number } { /* ... */ }
// Масові операції
clearCompleted(): void { /* ... */ }
toggleAll(): void { /* ... */ }
// Рендеринг
render(): void { /* ... */ }
}
5. Type-safe Event Handlers
// Обробка submit форми
function handleSubmit(event: SubmitEvent): void {
event.preventDefault();
// ...
}
// Обробка click на todo (делегування подій)
function handleTodoClick(event: MouseEvent): void {
const target = event.target as HTMLElement;
const todoItem = target.closest<HTMLLIElement>("[data-id]");
if (!todoItem) return;
const id = todoItem.dataset.id;
if (!id) return;
// Визначаємо дію
if (target.classList.contains("toggle-btn")) {
app.toggleTodo(id);
} else if (target.classList.contains("delete-btn")) {
app.removeTodo(id);
}
}
// Обробка зміни фільтра
function handleFilterChange(event: MouseEvent): void {
const target = event.target as HTMLButtonElement;
const filter = target.dataset.filter as Filter;
if (filter) {
app.setFilter(filter);
}
}
// Debounced пошук
function handleSearchInput(event: Event): void {
const target = event.target as HTMLInputElement;
app.setSearchQuery(target.value);
}
6. HTML-файл
Створи index.html з необхідною структурою:
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Todo List (TypeScript)</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="app">
<h1>Todo List</h1>
<form id="todo-form">
<input id="todo-input" type="text" placeholder="Нове завдання..." />
<select id="priority-select">
<option value="low">Низький</option>
<option value="medium">Середній</option>
<option value="high">Високий</option>
</select>
<button type="submit">Додати</button>
</form>
<input id="search-input" type="text" placeholder="Пошук..." />
<div class="filters">
<button class="filter-btn active" data-filter="all">Всі</button>
<button class="filter-btn" data-filter="active">Активні</button>
<button class="filter-btn" data-filter="completed">Завершені</button>
</div>
<select id="sort-select">
<option value="date">За датою</option>
<option value="priority">За пріоритетом</option>
<option value="alphabetical">За алфавітом</option>
</select>
<ul id="todo-list"></ul>
<div class="footer">
<span id="todo-counter">0 завдань</span>
<button id="clear-completed">Очистити завершені</button>
</div>
</div>
<script src="dist/app.js" type="module"></script>
</body>
</html>
Вимоги до типізації
- Жодного
anyу коді - Всі функції мають типізовані параметри та return types
- DOM-елементи типізовані конкретними типами (не просто
HTMLElement) - Events типізовані (
MouseEvent,SubmitEvent,Event) - localStorage обернутий у generic
TypedStorage - Enum для
PriorityтаFilter - Literal types для
SortByтаSortOrder
Налаштування проєкту
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"noEmitOnError": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
// package.json scripts
{
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}
Бонус
- Додай drag-and-drop для зміни порядку (з типізацією
DragEvent) - Реалізуй Undo/Redo через паттерн Command з типізованими командами
- Додай експорт/імпорт todos у JSON з runtime-валідацією через type guard
- Створи theme switcher (dark/light) з типізованим стейтом
Критерії оцінки
| Критерій | Бали |
|---|---|
| Інтерфейси та типи (Todo, AppState, enums) | 15 |
| Generic TypedStorage для localStorage | 15 |
| Типізовані DOM-елементи (getElement, конкретні типи) | 10 |
| CRUD операції (add/remove/toggle/edit) | 15 |
| Фільтрація, сортування, пошук | 15 |
| Type-safe event handlers (submit, click, input) | 10 |
| Рендеринг з коректними типами | 10 |
| Жодного any, strict: true, код компілюється без помилок | 10 |
| Бонус: Drag-and-drop / Undo-Redo / Export-Import / Themes | +20 |