Интерфейсы vs type aliases
Когда что использовать — сравнение двух подходов
Цели урока
- Понять различия между interface и type
- Узнать, что может только interface
- Узнать, что может только type
- Научиться выбирать правильный инструмент
- Понять, когда оба варианта эквивалентны
Два способа описания типов
TypeScript предоставляет два основных способа описания структуры данных: interface и type. Оба позволяют описывать объекты, но имеют важные различия.
interface User {
name: string;
age: number;
}
// Type alias — псевдоним для любого типа
type UserType = {
name: string;
age: number;
};
Для простых случаев оба варианта работают одинаково. Но есть ситуации, когда один подходзаметный лучше другого. Давайте рассмотрим каждую возможность по отдельности.
Основное различие заключается в том, что interface предназначен для описания структуры объектов и поддерживает declaration merging, а type — более гибкий инструмент для создания псевдонимов любых типов.
Что может только interface?
Интерфейсы имеют несколько уникальных возможностей:
// Это УНИКАЛЬНАЯ возможность интерфейсов!
interface Window {
myApp: string;
}
// Где-то в другом файле
interface Window {
analytics: object;
}
// Теперь Window содержит И myApp, И analytics!
// Это невозможно с type alias!
// 2. Расширение через extends
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
// 3. Implements в классах
class MyDog implements Dog {
name = "Шарик";
breed = "Лабрадор";
}
Это возможность расширять интерфейс, объявляя его повторно. TypeScript автоматически объединит все объявления. Это особенно полезно при расширении типов сторонних библиотек или добавлении собственных свойств к глобальным типам.
Declaration merging широко используется в экосистеме TypeScript. Например, Express.js добавляет свойства к объекту Request и Response через declaration merging. React добавляет JSX typings аналогичным образом.
Что может только type?
Type aliases имеют свои уникальные возможности:
type StringOrNumber = string | number;
type Status = "active" | "inactive" | "pending";
// 2. Intersection-типы
type HasName = { name: string; };
type HasAge = { age: number; };
type Person = HasName & HasAge;
// 3. Типы примитивов
type ID = string | number;
type Callback = (data: any) => void;
type Primitive = string | number | boolean | null | undefined;
// 4. Кортежи
type Point = [number, number];
type KeyValuePair<K, V> = [K, V];
// 5. Mapped-типы
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
// 6. Условные типы
type IsString<T> = T extends string ? true : false;
// 7. Утилитарные типы
type Partial<T> = { [K in keyof T]?: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };
Type aliases гораздо более гибкие и могут описывать любые типы, включая примитивы, union-типы, кортежи и сложные вычисляемые типы. Это делает их незаменимыми для многих сценариев.
Сравнительная таблица
| Возможность | interface | type |
|---|---|---|
| Описание объектов | ✓ | ✓ |
| Наследование | extends | & (intersection) |
| Declaration merging | ✓ | ✗ |
| Union-типы | ✗ | ✓ |
| Кортежи | ✗ | ✓ |
| Mapped-типы | ✗ | ✓ |
| Условные типы | ✗ | ✓ |
| Implements в классах | ✓ | ✓ |
Declaration merging: глубокое погружение
Это одна из самых мощных и уникальных возможностей интерфейсов:
interface Window {
analytics: {
track(event: string): void;
};
env: {
API_URL: string;
};
}
// Теперь window.analytics и window.env доступны
// TypeScript знает об этих свойствах!
window.analytics.track("page_view");
console.log(window.env.API_URL);
Это широко используется в библиотеках для расширения глобальных типов. Например, Express добавляет Request и Response через declaration merging. Next.js расширяет типы для getServerSideProps.
Когда выбирать interface?
Используйте interface, когда:
- Описываете структуру объектов (API-ответы, конфигурации, данные)
- Нужно расширять типы через
extends - Работаете с классами и нужен
implements - Нужно declaration merging (расширение глобальных типов)
- Создаёте контракты для объектов
- Описываете структуру React-компонентов (props)
Когда выбирать type?
Используйте type, когда:
- Нужны union-типы:
type Status = "active" | "inactive" - Нужны кортежи:
type Point = [number, number] - Нужны mapped-типы или условные типы
- Описываете примитивы:
type ID = string | number - Создаёте утилитарные типы
- Нужно комбинировать типы через
& - Описываете функции:
type Fn = (x: number) => string
Практическое сравнение
Рассмотрим реальный пример: описание API-клиента.
interface User {
id: number;
name: string;
email: string;
}
// Используем type для union-типа
type UserRole = "admin" | "user" | "guest";
// Используем interface для расширения
interface AdminUser extends User {
role: "admin";
permissions: string[];
}
// Используем type для комбинации
type ApiResponse<T> = {
status: number;
data: T;
} & { timestamp: number; };
Как видите, interface и type дополняют друг друга. Используйте их там, где они наиболее эффективны.
Паттерны использования
fetch(): Promise<any>;
}
// Расширяем в другом месте
interface MyApi {
post(): Promise<any>;
}
| { success: true; data: T; }
| { success: false; error: string; };
// Type narrowing работает!
function handle(r: Result<any>) {
if (r.success) { /* r.data */ }
else { /* r.error */ }
}
compareTo(other: T): number;
}
class Number implements Comparable<Number> {
compareTo(o: Number) { ... }
}
get[K in keyof T]
(): T[K];
};
// Автоматически создаёт геттеры
// для всех свойств
Распространённые ошибки
"active" | "inactive";
}
// Ошибка синтаксиса!
"active" | "inactive";
// OK!
type MyType = { y: number; };
// Ошибка: дублирование!
interface MyType { y: number; }
// OK! Автоматическое слияние
Практическое сравнение: React-компоненты
В React оба подхода используются, но в разных контекстах:
// (поддерживает declaration merging для расширения)
interface ButtonProps {
label: string;
onClick: () => void;
variant?: ButtonVariant;
}
// Type для union-типов
type ButtonVariant = "primary" | "secondary" | "danger";
// Type для состояний
type ButtonState = "idle" | "loading" | "success" | "error";
// Interface для контекста
interface AuthContext {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
В React-проектах interface обычно используется для пропсов и контекстов, а type — для union-типов и состояний. Это общепринятая практика в экосистеме React.
Практическое сравнение: API-клиенты
При работе с API оба подхода дополняют друг друга:
interface User {
id: number;
name: string;
email: string;
}
// Type для параметров запроса
type UserFilters = {
name?: string;
email?: string;
role?: UserRole;
} & Pagination;
// Type для ответа API
type ApiResponse<T> =
| { success: true; data: T; }
| { success: false; error: string; };
// Interface для API-клиента
interface UserApi {
getById(id: number): Promise<ApiResponse<User>>;
list(filters?: UserFilters): Promise<ApiResponse<User[]>>;
create(data: Omit<User, "id">): Promise<ApiResponse<User>>;
}
В API-клиентах interface используется для описания методов, а type — для union-типов и комбинаций. Это создаёт чёткую структуру кода.
Практическое задание
Проанализируйте следующий код и определите, где лучше использовать interface, а где — type:
// 2. Union-тип для статусов
// 3. Расширение интерфейса для администратора
// 4. Кортеж для координат
// 5. Дженерик для API-ответа
// 6. Intersection-тип для комбинации
// 7. Callable interface для функции
// 8. Mapped-тип для валидации
Чек-лист урока
- Interface — для описания структуры объектов и расширения
- Type — для union-типов, кортежей, mapped-типов
- Interface поддерживает declaration merging
- Type поддерживает union и intersection типы
- Interface лучше для контрактов классов (implements)
- Type лучше для сложных вычисляемых типов
- Используйте оба подхода там, где они наиболее эффективны
- Для простых объектов оба варианта эквивалентны
Тест по интерфейсам vs type aliases
8 вопросов