Пошук уроків, статей та іншого контенту
Порівнюємо interface і type alias та визначаємо, який підхід обрати для різних моделей.
У TypeScript interface і type можуть описувати форму об’єкта:
interface User {
id: number;
name: string;
}
type Product = {
id: number;
title: string;
};У більшості простих випадків ці записи працюють однаково:
const user: User = {
id: 1,
name: "Anna",
};
const product: Product = {
id: 10,
title: "Keyboard",
};Обидва підходи:
перевіряють структуру об’єктів;
підтримують необов’язкові властивості через ?;
підтримують властивості лише для читання через readonly;
можуть описувати методи;
можуть використовуватися з implements у класах.
Різниця стає помітною, коли потрібно комбінувати типи, розширювати їх або повторно оголошувати.
interfaceinterface найкраще підходить для опису форми об’єктів і публічних контрактів.
interface User {
readonly id: number;
name: string;
email?: string;
}
function printUser(user: User): void {
console.log(`${user.id}: ${user.name}`);
}
printUser({
id: 1,
name: "Anna",
});Інтерфейс можна розширити за допомогою extends:
interface Person {
name: string;
}
interface Employee extends Person {
department: string;
}
const employee: Employee = {
name: "Oleh",
department: "Engineering",
};Employee успадковує всі властивості Person і додає власну властивість department.
Інтерфейс може розширювати кілька інтерфейсів:
interface HasId {
id: number;
}
interface HasTimestamps {
createdAt: Date;
updatedAt: Date;
}
interface Order extends HasId, HasTimestamps {
total: number;
}
const order: Order = {
id: 101,
createdAt: new Date(),
updatedAt: new Date(),
total: 2500,
};Такий синтаксис добре читається, коли модель поступово складається з кількох частин.
Інтерфейс можна оголосити кілька разів. TypeScript об’єднає всі оголошення:
interface WindowConfig {
theme: "light" | "dark";
}
interface WindowConfig {
width: number;
}
const config: WindowConfig = {
theme: "dark",
width: 1200,
};Ця можливість називається declaration merging.
Вона корисна, коли потрібно доповнити тип, створений у сторонній бібліотеці або іншому модулі. Проте в коді власного застосунку випадкове повторне оголошення може ускладнити пошук джерела властивостей.
typetype зручний, коли потрібно описати не лише об’єкт, а довільний тип TypeScript:
об’єднання типів;
перетин типів;
примітив;
кортеж;
тип функції;
умовний або обчислюваний тип.
Union дозволяє значенню мати один із кількох типів:
type RequestStatus = "idle" | "loading" | "success" | "error";
function showStatus(status: RequestStatus): string {
switch (status) {
case "idle":
return "Запит ще не розпочато";
case "loading":
return "Завантаження";
case "success":
return "Успішно";
case "error":
return "Помилка";
}
}
console.log(showStatus("success"));Інтерфейс не може безпосередньо описати такий union:
type ApiResult =
| { ok: true; data: string[] }
| { ok: false; error: string };У цьому прикладі форма результату залежить від значення ok. TypeScript може звужувати тип у перевірці:
function processResult(result: ApiResult): void {
if (result.ok) {
console.log(result.data.join(", "));
} else {
console.error(result.error);
}
}Intersection об’єднує вимоги кількох типів:
type Identified = {
id: number;
};
type Named = {
name: string;
};
type Category = Identified & Named;
const category: Category = {
id: 1,
name: "Books",
};Category повинен містити властивості і Identified, і Named.
Для типів, які не є формою об’єкта, type зазвичай є природнішим вибором:
type UserId = number;
type Formatter = (value: number) => string;
type Coordinates = [number, number];
const userId: UserId = 42;
const formatPrice: Formatter = (value) => `${value.toFixed(2)} грн`;
const point: Coordinates = [50.45, 30.52];
console.log(formatPrice(199.99));
console.log(point);Ці можливості однаково працюють для interface і type:
interface InterfaceOptions {
readonly id: number;
label?: string;
}
type TypeOptions = {
readonly id: number;
label?: string;
};
const interfaceOptions: InterfaceOptions = {
id: 1,
};
const typeOptions: TypeOptions = {
id: 2,
label: "Default",
};
// interfaceOptions.id = 3; // Помилка: властивість доступна лише для читанняreadonly захищає властивість на рівні перевірки типів. Це не робить об’єкт повністю незмінним під час виконання JavaScript.
Клас може реалізувати інтерфейс за допомогою implements:
interface Repository {
save(value: string): void;
}
class MemoryRepository implements Repository {
private values: string[] = [];
save(value: string): void {
this.values.push(value);
}
getAll(): string[] {
return [...this.values];
}
}
const repository = new MemoryRepository();
repository.save("first");
console.log(repository.getAll());Клас також може реалізувати сумісний об’єктний type:
type Logger = {
log(message: string): void;
};
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}Для простих контрактів класів обидва варіанти підходять. На практиці для публічного контракту об’єкта або класу часто обирають interface, а для складених типів — type.
interface підтримує повторне оголошення:
interface Settings {
language: string;
}
interface Settings {
timezone: string;
}
const settings: Settings = {
language: "uk",
timezone: "Europe/Kyiv",
};type не можна оголосити повторно з тим самим іменем:
type Options = {
enabled: boolean;
};
// type Options = { // Помилка повторного оголошення
// timeout: number;
// };extends та перетин &Для інтерфейсів розширення зазвичай записують через extends:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}Для type alias використовують intersection:
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};Обидва варіанти можуть бути доречними. extends чітко виражає відношення розширення, а & зручно використовувати під час складання складних типів.
Інтерфейс повинен отримати від батьківського типу коректну форму об’єкта. Union не можна безпосередньо розширити інтерфейсом:
type Result = string | number;
// interface InvalidResult extends Result {} // ПомилкаУ такій ситуації потрібно використати type:
type ResultWithMessage = Result & {
message: string;
};Для простих об’єктних моделей різниця зазвичай не помітна, але під час роботи з union, tuple або умовними типами type має ширші можливості.
Орієнтуйтеся на таке правило:
interface, якщо:описуєте форму об’єкта;
створюєте публічний контракт модуля;
описуєте клас, який має певний набір методів;
очікуєте можливе розширення через extends;
потрібне declaration merging.
interface PaymentProvider {
pay(amount: number): Promise<void>;
}type, якщо:описуєте union або intersection;
створюєте псевдонім для примітиву;
описуєте tuple;
описуєте тип функції;
використовуєте mapped, conditional або template literal types;
потрібно скласти тип із кількох варіантів.
type PaymentState =
| { status: "pending" }
| { status: "completed"; transactionId: string }
| { status: "failed"; reason: string };Для звичайної об’єктної моделі обидва рішення можуть бути правильними:
interface Article {
id: number;
title: string;
}
type ArticleDto = {
id: number;
title: string;
};У такому випадку важливіше дотримуватися єдиного стилю в межах проєкту, ніж шукати універсально «правильний» синтаксис.
У багатьох командах діє проста домовленість:
interface — для об’єктних контрактів;
type — для всіх інших типів і складених комбінацій.
Це не вимога TypeScript, а практичне правило для узгодженості коду.
Можна використовувати один тип як основу для іншого:
interface User {
id: number;
name: string;
email: string;
}
type UserPreview = Pick<User, "id" | "name">;
const preview: UserPreview = {
id: 1,
name: "Anna",
};UserPreview містить лише вибрані властивості User. Тут Pick працює з типом незалежно від того, був він оголошений через interface чи type.
Ще один приклад:
interface CreateUserInput {
name: string;
email: string;
}
type ReadonlyUserInput = Readonly<CreateUserInput>;
const input: ReadonlyUserInput = {
name: "Oleh",
email: "oleh@example.com",
};
// input.name = "Petro"; // Помилка: властивість доступна лише для читанняinterface лише тому, що це «об’єкт»Для простого об’єкта справді можна використати interface, але type також коректний. Не потрібно механічно переписувати всі type alias на інтерфейси.
type Config = {
port: number;
host: string;
};Цей тип не є проблемним лише через те, що він оголошений через type.
interface підтримує unionІнтерфейс не описує тип «одне або інше»:
type Id = number | string;Для union використовуйте type.
Повторне оголошення інтерфейсу може бути корисним для розширення типів бібліотек, але в основному коді воно іноді приховує структуру моделі.
Якщо всі властивості належать одній моделі, зазвичай зрозуміліше оголосити їх в одному місці.
extends і сумісністю типівextends описує спосіб побудови типу, але TypeScript також перевіряє структурну сумісність:
interface HasName {
name: string;
}
const value = {
name: "Anna",
age: 30,
};
const named: HasName = value;value можна присвоїти HasName, оскільки він має необхідну властивість name. Додаткова властивість age не заважає в цьому випадку.
readonlyreadonly не можна змінювати через тип:
type User = {
readonly id: number;
};
const user: User = { id: 1 };
// user.id = 2; // ПомилкаЯкщо потрібно створити змінювану версію типу, потрібно описати її окремо або використати відповідний тип-помічник.
interface і type можуть описувати форму об’єкта.
interface зручно використовувати для об’єктних контрактів, класів і розширення через extends.
type необхідний або зручніший для union, intersection, примітивів, функцій і кортежів.
interface підтримує declaration merging, а type повторно оголосити не можна.
Для об’єктних типів обидва підходи часто рівноцінні.
Якщо немає технічної причини обрати конкретний варіант, дотримуйтеся стилю, прийнятого у вашому проєкті.