$ sudo teach IT
МОДУЛЬ 5 · УРОК 6

Интерфейсы vs type aliases

Когда что использовать — сравнение двух подходов

Цели урока

  • Понять различия между interface и type
  • Узнать, что может только interface
  • Узнать, что может только type
  • Научиться выбирать правильный инструмент
  • Понять, когда оба варианта эквивалентны

Два способа описания типов

TypeScript предоставляет два основных способа описания структуры данных: interface и type. Оба позволяют описывать объекты, но имеют важные различия.

// Interface — описание структуры объекта
interface User {
  name: string;
  age: number;
}

// Type alias — псевдоним для любого типа
type UserType = {
  name: string;
  age: number;
};

Для простых случаев оба варианта работают одинаково. Но есть ситуации, когда один подходзаметный лучше другого. Давайте рассмотрим каждую возможность по отдельности.

Основное различие заключается в том, что interface предназначен для описания структуры объектов и поддерживает declaration merging, а type — более гибкий инструмент для создания псевдонимов любых типов.

Что может только interface?

Интерфейсы имеют несколько уникальных возможностей:

// 1. Declaration merging (слияние объявлений)
// Это УНИКАЛЬНАЯ возможность интерфейсов!
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 = "Лабрадор";
}
Declaration merging:

Это возможность расширять интерфейс, объявляя его повторно. TypeScript автоматически объединит все объявления. Это особенно полезно при расширении типов сторонних библиотек или добавлении собственных свойств к глобальным типам.

Declaration merging широко используется в экосистеме TypeScript. Например, Express.js добавляет свойства к объекту Request и Response через declaration merging. React добавляет JSX typings аналогичным образом.

Что может только type?

Type aliases имеют свои уникальные возможности:

// 1. Union-типы — УНИКАЛЬНАЯ возможность type!
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: глубокое погружение

Это одна из самых мощных и уникальных возможностей интерфейсов:

// Файл: types/global.d.ts
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 для объектов
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 дополняют друг друга. Используйте их там, где они наиболее эффективны.

Паттерны использования

Паттерн: расширение API
interface MyApi {
  fetch(): Promise<any>;
}

// Расширяем в другом месте
interface MyApi {
  post(): Promise<any>;
}
Паттерн: union-тип
type Result<T> =
  | { success: true; data: T; }
  | { success: false; error: string; };

// Type narrowing работает!
function handle(r: Result<any>) {
  if (r.success) { /* r.data */ }
  else { /* r.error */ }
}
Паттерн: класс
interface Comparable<T> {
  compareTo(other: T): number;
}

class Number implements Comparable<Number> {
  compareTo(o: Number) { ... }
}
Паттерн: mapped-тип
type Getters<T> = {
  get[K in keyof T]
  (): T[K];
};

// Автоматически создаёт геттеры
// для всех свойств

Распространённые ошибки

Ошибка: interface для union-типа
interface Status {
  "active" | "inactive";
}
// Ошибка синтаксиса!
Правильно: type для union
type Status =
  "active" | "inactive";
// OK!
Ошибка: type для declaration merging
type MyType = { x: number; };
type MyType = { y: number; };
// Ошибка: дублирование!
Правильно: interface для merging
interface MyType { x: number; }
interface MyType { y: number; }
// OK! Автоматическое слияние

Практическое сравнение: React-компоненты

В React оба подхода используются, но в разных контекстах:

// Interface для пропсов компонента
// (поддерживает 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 для структуры данных
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:

// 1. Описание структуры пользователя
// 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 вопросов

Статус заказа

Premium