Вивчай

Інтерфейси

TypeScript інтерфейси — архітектурне креслення з точними розмірами як контракт для структури об'єктаTypeScript інтерфейси — архітектурне креслення з точними розмірами як контракт для структури об'єкта

У попередньому уроці ми використовували type для опису структури об'єктів. TypeScript має ще один інструмент для цього -- інтерфейси (interface). Інтерфейси -- один з найважливіших інструментів TypeScript для опису "контрактів" -- якою має бути форма об'єкта.


Що таке interface?

Interface описує структуру об'єкта -- які властивості та методи він повинен мати:

interface User {
  name: string;
  email: string;
  age: number;
}

// Об'єкт повинен відповідати інтерфейсу
const user: User = {
  name: "Олексій",
  email: "alex@test.com",
  age: 25,
};

// Помилки:
// const bad: User = { name: "Олексій" }; // Помилка: не вистачає email та age
// const bad2: User = { ...user, extra: true }; // Помилка: зайва властивість

Використання з функціями

interface User {
  name: string;
  email: string;
  age: number;
}

function greetUser(user: User): string {
  return `Привіт, ${user.name}! Тобі ${user.age} років.`;
}

function createUser(name: string, email: string, age: number): User {
  return { name, email, age };
}

const user = createUser("Марія", "maria@test.com", 30);
console.log(greetUser(user)); // "Привіт, Марія! Тобі 30 років."

Optional Properties -- необов'язкові властивості

Знак ? робить властивість необов'язковою:

interface User {
  name: string;
  email: string;
  age?: number;        // може бути number або undefined
  phone?: string;      // може бути string або undefined
}

// Усі ці варіанти валідні:
const user1: User = { name: "Олексій", email: "alex@test.com" };
const user2: User = { name: "Олексій", email: "alex@test.com", age: 25 };
const user3: User = { name: "Олексій", email: "alex@test.com", age: 25, phone: "+380..." };

Робота з optional properties

function getUserAge(user: User): string {
  // user.age може бути undefined!
  if (user.age !== undefined) {
    return `${user.name}: ${user.age} років`;
  }
  return `${user.name}: вік не вказано`;

  // Або коротше з nullish coalescing (з Block 6):
  // return `${user.name}: ${user.age ?? "вік не вказано"}`;
}

Readonly Properties -- тільки для читання

readonly забороняє зміну властивості після створення об'єкта:

interface Config {
  readonly apiUrl: string;
  readonly apiKey: string;
  timeout: number; // можна змінювати
}

const config: Config = {
  apiUrl: "https://api.example.com",
  apiKey: "secret-key-123",
  timeout: 5000,
};

config.timeout = 10000;    // OK
// config.apiUrl = "new";  // Помилка: Cannot assign to 'apiUrl' because it is a read-only property
Інфо

readonly -- це перевірка тільки на рівні TypeScript. У скомпільованому JavaScript ніяких обмежень не буде. Але під час розробки це дуже корисно для запобігання випадковим змінам.


Методи в інтерфейсах

Інтерфейси можуть описувати не тільки властивості, а й методи:

interface User {
  name: string;
  email: string;
  age: number;

  // Метод -- два способи запису
  greet(): string;
  sendEmail: (subject: string, body: string) => boolean;
}

const user: User = {
  name: "Олексій",
  email: "alex@test.com",
  age: 25,

  greet() {
    return `Привіт, я ${this.name}!`;
  },

  sendEmail(subject, body) {
    console.log(`Sending "${subject}" to ${this.email}`);
    return true;
  },
};

Extending Interfaces -- розширення

Інтерфейси можна розширювати (наслідувати) один від одного:

interface User {
  name: string;
  email: string;
  age: number;
}

// Admin "наслідує" всі властивості User і додає свої
interface Admin extends User {
  permissions: string[];
  level: number;
}

const admin: Admin = {
  name: "Марія",
  email: "maria@test.com",
  age: 30,
  permissions: ["users", "content"],
  level: 2,
};

// Множинне наслідування
interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface BlogPost extends User, Timestamps {
  title: string;
  content: string;
}

Практичний приклад: API response

interface ApiResponse {
  success: boolean;
  timestamp: string;
}

interface UserResponse extends ApiResponse {
  data: User;
}

interface UsersListResponse extends ApiResponse {
  data: User[];
  total: number;
  page: number;
}

// Використання
function handleResponse(response: UserResponse): void {
  if (response.success) {
    console.log(`Користувач: ${response.data.name}`);
  }
}

Index Signatures -- індексні підписи

Коли не знаєш точних імен ключів, але знаєш їхні типи:

// Об'єкт з будь-якими ключами-рядками та числовими значеннями
interface Scores {
  [subject: string]: number;
}

const studentScores: Scores = {
  math: 95,
  english: 87,
  physics: 92,
};

studentScores.chemistry = 88; // OK

// Комбінація з фіксованими полями
interface UserSettings {
  theme: "light" | "dark";
  language: string;
  [key: string]: string; // додаткові налаштування
}
Увага

Будь обережний з index signatures: вони дозволяють будь-який ключ. TypeScript не скаже про помилку, якщо ти звернешся до неіснуючого ключа: scores.nonexistent поверне undefined, але TypeScript покаже тип number.


Declaration Merging -- злиття інтерфейсів

Унікальна особливість interface (якої немає у type): якщо оголосити два інтерфейси з однаковим ім'ям, вони зливаються:

interface User {
  name: string;
}

interface User {
  email: string;
}

// TypeScript об'єднує обидва оголошення:
// User = { name: string; email: string }
const user: User = {
  name: "Олексій",
  email: "alex@test.com",
};

Це корисно для розширення типів з бібліотек, але в повсякденному коді може заплутати. Використовуй цю можливість обережно.


Interface vs Type -- коли що використовувати?

Обидва інструменти вирішують схожі задачі, але мають відмінності:

// interface -- для опису об'єктів
interface User {
  name: string;
  email: string;
}

// type -- для будь-яких типів
type ID = string | number;           // union -- тільки type!
type Theme = "light" | "dark";       // literal union -- тільки type!
type Coordinate = [number, number];  // tuple -- тільки type!

Порівняння

Можливістьinterfacetype
Опис об'єктаТакТак
Extending (розширення)extends& (intersection)
Union typesНіТак
Literal typesНіТак
TuplesНіТак
Declaration mergingТакНі
Implements (для класів)ТакТак

Розширення: extends vs intersection

// interface + extends
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// type + intersection (&)
type Animal2 = {
  name: string;
};
type Dog2 = Animal2 & {
  breed: string;
};

// Обидва варіанти дають однаковий результат

Рекомендація

Порада

Просте правило:

  • Використовуй interface для опису об'єктів та класів
  • Використовуй type для union types, literal types, tuples та складних комбінацій
  • Якщо сумніваєшся -- використовуй interface для об'єктів, type для всього іншого

Більшість команд дотримуються саме цього підходу.


Практика: система інтернет-магазину

interface Product {
  readonly id: number;
  title: string;
  price: number;
  description?: string;
  category: "electronics" | "clothing" | "books";
}

interface CartItem {
  product: Product;
  quantity: number;
}

interface Cart {
  items: CartItem[];
  readonly createdAt: Date;

  addItem(product: Product, quantity: number): void;
  removeItem(productId: number): void;
  getTotal(): number;
}

// Реалізація
const cart: Cart = {
  items: [],
  createdAt: new Date(),

  addItem(product, quantity) {
    const existing = this.items.find((item) => item.product.id === product.id);
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
  },

  removeItem(productId) {
    this.items = this.items.filter((item) => item.product.id !== productId);
  },

  getTotal() {
    return this.items.reduce(
      (sum, item) => sum + item.product.price * item.quantity,
      0
    );
  },
};

// Використання
const laptop: Product = {
  id: 1,
  title: "Ноутбук",
  price: 25000,
  category: "electronics",
};

cart.addItem(laptop, 2);
console.log(cart.getTotal()); // 50000

Підсумок

  • interface -- описує структуру (контракт) об'єкта
  • Optional (?) -- необов'язкова властивість, може бути undefined
  • readonly -- забороняє зміну після створення
  • extends -- розширення інтерфейсу (наслідування)
  • Index signatures ([key: string]: type) -- динамічні ключі
  • Declaration merging -- два інтерфейси з одним ім'ям зливаються
  • interface -- для об'єктів; type -- для union, literal, tuple

Що далі?

У наступному уроці ми навчимося типізувати функції в TypeScript: параметри, return types, function types, overloading. Це дозволить писати функції, які гарантують правильне використання.

Інфо

Корисні посилання: