Введение: вечный спор TypeScript-разработчиков
Если вы хоть раз писали на TypeScript, то наверняка сталкивались с вопросом: "Когда использовать interface, а когда type?" Этот вопрос собрал тысячи просмотров на Stack Overflow и стал настоящим камнем преткновения для новичков. Давайте раз и навсегда разберемся в этой дилемме.
TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. И у нас есть два основных инструмента для описания структуры данных: Interface и Type Alias. На первый взгляд они делают одно и то же, но дьявол кроется в деталях. В этой статье мы не просто перечислим различия — мы покажем их на реальных примерах и дадим четкие рекомендации.
Что такое Interface?
Interface — это способ описать структуру объекта. Он как чертеж для данных: вы говорите TypeScript, какие свойства и методы должен иметь объект.
// Базовый интерфейс
interface User {
name: string;
age: number;
email?: string; // необязательное свойство
readonly id: number; // только для чтения
}
// Использование
const user: User = {
name: "Анна",
age: 25,
id: 1
};
Интерфейсы могут наследоваться друг от друга, что делает их идеальными для описания иерархий:
interface Animal {
name: string;
sound(): void;
}
interface Dog extends Animal {
breed: string;
wagTail(): void;
}
// Теперь Dog содержит все поля Animal + свои
const myDog: Dog = {
name: "Рекс",
breed: "Овчарка",
sound() { console.log("Гав!"); },
wagTail() { console.log("Виляет хвостом"); }
};
Что такое Type Alias?
Type Alias (псевдоним типа) — это более гибкий инструмент. Он может описывать не только объекты, но и примитивы, объединения и пересечения типов.
// Тип для примитива
type ID = string | number;
// Тип для объекта
type UserType = {
name: string;
age: number;
};
// Тип для функции
type Greeting = (name: string) => string;
// Union Type (объединение)
type Status = "active" | "inactive" | "pending";
// Intersection Type (пересечение)
type Person = { name: string };
type Employee = Person & { salary: number };
Главная суперсила типов — union types. Интерфейсы такое не умеют:
// ❌ Так нельзя с interface
// interface Status = "active" | "inactive"; // Ошибка!
// ✅ А с type — легко!
type Status = "active" | "inactive" | "pending";
function processStatus(status: Status) {
switch (status) {
case "active":
console.log("Активен");
break;
case "inactive":
console.log("Неактивен");
break;
case "pending":
console.log("В ожидании");
break;
}
}
Ключевые различия: Interface vs Type
Давайте разложим по полочкам, чем они отличаются на практике.
1. Объединение и пересечение типов
Type поддерживает union (|) и intersection (&) напрямую. Interface — только через extends.
// Union — только type
type Success = { data: string };
type Error = { error: string };
type Result = Success | Error; // Можно!
// Intersection
type Full = Success & Error; // Можно!
// Для interface пришлось бы создавать отдельные интерфейсы
interface IResult extends Success, Error {} // Работает, но громоздко
2. Declaration merging (слияние объявлений)
Это уникальная фича интерфейсов. Если вы объявите два интерфейса с одним именем, TypeScript объединит их.
// Первый интерфейс
interface User {
name: string;
}
// Второй интерфейс с тем же именем
interface User {
age: number;
}
// Результат: User = { name: string; age: number; }
const user: User = {
name: "Иван",
age: 30
};
С типами такое не пройдет — будет ошибка:
type User = { name: string };
type User = { age: number }; // ❌ Duplicate identifier 'User'
3. Работа с примитивами
Type может создавать псевдонимы для примитивов, интерфейсы — нет.
type UserID = number; // ✅ Можно
type Callback = (x: number) => void; // ✅ Можно
// interface UserID = number; // ❌ Нельзя
// interface Callback = (x: number) => void; // ❌ Нельзя
4. Mapped Types (маппинг типов)
Type поддерживает маппинг — мощный инструмент для трансформации типов:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
// { readonly name: string; readonly age: number; }
type OptionalPerson = Optional<Person>;
// { name?: string; age?: number; }
Интерфейсы с этим справляются хуже — приходится использовать утилитарные типы.
Когда что выбирать? Практические рекомендации
Теперь самый важный вопрос: что использовать в реальных проектах? Вот мой чек-лист:
Используйте Interface, когда:
- Вы пишете библиотеку — интерфейсы лучше расширяются пользователями через declaration merging.
- Нужна иерархия классов — интерфейсы отлично сочетаются с
implements. - Вы работаете с объектами — интерфейсы более читаемы для описания структур.
- Важно наследование — extends работает интуитивно понятнее.
Используйте Type, когда:
- Нужны union или intersection — это главное преимущество типов.
- Работаете с примитивами — создавайте псевдонимы для строк, чисел и т.д.
- Нужны сложные вычисляемые типы — mapped types, conditional types.
- Пишете утилиты — типы лучше подходят для обобщенных решений.
Реальный пример из жизни
Представьте, что вы пишете систему управления задачами. Вот как может выглядеть типизация с использованием обоих подходов:
// Базовые типы
type TaskStatus = "todo" | "in_progress" | "done";
type Priority = "low" | "medium" | "high";
// Интерфейс для задачи
interface Task {
id: string;
title: string;
description?: string;
status: TaskStatus;
priority: Priority;
createdAt: Date;
}
// Функция для обновления статуса
function updateTaskStatus(
task: Task,
newStatus: TaskStatus
): Task {
return { ...task, status: newStatus };
}
// Union для результата операции
type TaskResult =
| { success: true; task: Task }
| { success: false; error: string };
// Пример использования
function createTask