Інтерфейси
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!
Порівняння
| Можливість | interface | type |
|---|---|---|
| Опис об'єкта | Так | Так |
| 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. Це дозволить писати функції, які гарантують правильне використання.
Корисні посилання:
- TypeScript Handbook: Object Types -- все про об'єктні типи
- TypeScript Handbook: Interfaces -- офіційна документація
- Interface vs Type -- офіційне порівняння
- Matt Pocock: Interface vs Type -- практичні рекомендації