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

Інтерфейси для моделей даних

Створи систему інтерфейсів для інтернет-магазину: User, Product, Order. Використай optional/readonly властивості, extending, generics та utility types.


Завдання

1. Базові інтерфейси

Створи наступні інтерфейси:

// Мітки часу (використовується в інших інтерфейсах)
interface Timestamps {
  readonly createdAt: Date;
  updatedAt: Date;
}

// Користувач
interface User extends Timestamps {
  readonly id: number;
  name: string;
  email: string;
  role: "customer" | "seller" | "admin";
  phone?: string;          // optional
  address?: Address;       // optional
}

// Адреса
interface Address {
  city: string;
  street: string;
  zip: string;
  country?: string;        // optional, default "Україна"
}

// Товар
interface Product extends Timestamps {
  readonly id: number;
  title: string;
  description: string;
  price: number;           // в копійках (цілі числа!)
  category: ProductCategory;
  inStock: boolean;
  quantity: number;
  sellerId: number;        // ID продавця (User з role "seller")
  tags?: string[];         // optional
}

// Категорія товару
type ProductCategory = "electronics" | "clothing" | "books" | "food" | "other";

2. Замовлення

// Елемент замовлення
interface OrderItem {
  product: Pick<Product, "id" | "title" | "price">;  // тільки потрібні поля
  quantity: number;
  totalPrice: number;       // price * quantity
}

// Статус замовлення
type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";

// Замовлення
interface Order extends Timestamps {
  readonly id: number;
  userId: number;
  items: OrderItem[];       // мінімум 1 елемент
  status: OrderStatus;
  totalAmount: number;      // сума всіх items
  shippingAddress: Address;
  notes?: string;
}

3. CRUD типи з Utility Types

Створи типи для створення та оновлення сутностей:

// Для створення -- без id та timestamps (вони генеруються автоматично)
type CreateUser = // Omit<User, ...>

// Для оновлення -- всі поля optional, крім id
type UpdateUser = // Partial<...> & Pick<...>

// Аналогічно для Product та Order
type CreateProduct = // ...
type UpdateProduct = // ...

type CreateOrder = // ...
type UpdateOrder = // ...

4. Generic API Response

// Відповідь API
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

// Відповідь зі списком та пагінацією
interface PaginatedResponse<T> extends ApiResponse<T[]> {
  total: number;
  page: number;
  perPage: number;
  totalPages: number;
}

// Відповідь з помилкою
interface ApiError {
  success: false;
  error: string;
  code: number;
  details?: Record<string, string>;  // поле -> повідомлення про помилку
}

5. Функції для роботи з даними

Реалізуй наступні функції з правильними типами:

// Створити замовлення
function createOrder(userId: number, items: OrderItem[], address: Address): CreateOrder {
  // ...
}

// Обчислити загальну суму замовлення
function calculateOrderTotal(items: OrderItem[]): number {
  // ...
}

// Фільтрувати товари за категорією
function filterProducts(products: Product[], category: ProductCategory): Product[] {
  // ...
}

// Знайти товари в наявності
function getInStockProducts(products: Product[]): Product[] {
  // ...
}

// Отримати публічний профіль (без email та phone)
function getPublicProfile(user: User): Pick<User, "id" | "name" | "role"> {
  // ...
}

// Згрупувати замовлення за статусом
function groupOrdersByStatus(orders: Order[]): Record<OrderStatus, Order[]> {
  // ...
}

6. Generic Storage Interface

// Generic CRUD storage
interface Storage<T extends { id: number }> {
  getAll(): T[];
  getById(id: number): T | undefined;
  create(item: Omit<T, "id" | "createdAt" | "updatedAt">): T;
  update(id: number, updates: Partial<Omit<T, "id" | "createdAt">>): T | undefined;
  delete(id: number): boolean;
  find(predicate: (item: T) => boolean): T[];
}

Реалізуй createStorage<T>() функцію, яка повертає об'єкт, що реалізує Storage<T>. Автоматично генеруй id, createdAt, updatedAt.


Приклад використання

// Створення storage
const userStorage = createStorage<User>();
const productStorage = createStorage<Product>();

// Створення користувача
const user = userStorage.create({
  name: "Олексій",
  email: "alex@test.com",
  role: "customer",
});
// user.id -- автоматично згенерований
// user.createdAt -- автоматично

// Оновлення
userStorage.update(user.id, { phone: "+380123456789" });

// Пошук
const admins = userStorage.find((u) => u.role === "admin");

// Створення замовлення
const laptop: Product = productStorage.create({
  title: "Ноутбук",
  description: "Потужний ноутбук",
  price: 2500000, // 25000.00 грн у копійках
  category: "electronics",
  inStock: true,
  quantity: 10,
  sellerId: 1,
});

const order = createOrder(user.id, [
  {
    product: { id: laptop.id, title: laptop.title, price: laptop.price },
    quantity: 1,
    totalPrice: laptop.price,
  },
], {
  city: "Київ",
  street: "Хрещатик, 1",
  zip: "01001",
});

console.log(calculateOrderTotal(order.items));
console.log(getPublicProfile(user));

Бонус

  • Додай Validator<T> interface з методом validate(data: unknown): data is T та реалізуй валідатори для User, Product, Order
  • Створи type DeepReadonly<T> -- рекурсивний Readonly, який робить readonly і вкладені об'єкти
  • Додай подію onChange у Storage, яка повідомляє про зміни (Observer pattern з generics)
  • Створи type RequireAtLeastOne<T> -- тип, де хоча б одне поле обов'язкове

Критерії оцінки

КритерійБали
Базові інтерфейси (User, Address, Product, Timestamps)15
Інтерфейси замовлення (Order, OrderItem, OrderStatus)15
CRUD типи з Utility Types (Create/Update для кожної сутності)15
Generic ApiResponse та PaginatedResponse10
Функції з правильними типами (6 функцій)20
Generic Storage з реалізацією createStorage25
Бонус: Validator / DeepReadonly / onChange / RequireAtLeastOne+20

Пов'язані уроки