Вивчай
Домашнє завдання #20 ·
балівintermediate

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 для localStorage15
Типізовані 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