type или interface в TypeScript: что и когда использовать
Практика3 минРедакция Innovialabs
Коротко
Для описания формы объекта type и interface почти взаимозаменяемы. interface нужен, когда важны extends и слияние объявлений, type — для union, кортежей и вычисляемых типов. Главное — договориться о правиле в команде.
Короткий ответ
Для обычного объекта разницы почти нет:
type UserT = { id: number; name: string };
interface UserI { id: number; name: string }
Оба варианта работают одинаково в функциях, классах и пропсах компонентов. Различия проявляются в четырёх местах: слияние объявлений, union и другие не-объектные типы, extends против пересечений и сообщения об ошибках.
Различие 1: слияние объявлений
Два interface с одним именем автоматически объединяются:
interface Config { apiUrl: string }
interface Config { timeout: number }
const c: Config = { apiUrl: "/api", timeout: 5000 }; // оба поля обязательны
С type так нельзя — повторное объявление даст ошибку Duplicate identifier.
Когда это полезно: расширение чужих типов — например, добавить поле в глобальный Window или в типы библиотеки. Когда вредно: случайное совпадение имён в разных файлах молча меняет тип.
Различие 2: union и не-объектные типы
interface описывает только форму объекта. type умеет всё остальное:
type Status = "idle" | "loading" | "error";
type Id = string | number;
type Point = [number, number];
type Handler = (event: MouseEvent) => void;
type ReadonlyUser = Readonly<UserT>;
type Result =
| { ok: true; data: string }
| { ok: false; error: string };
Union-типы, кортежи, mapped и conditional types — территория type.
Различие 3: extends против пересечений
Наследование можно записать двумя способами:
interface Base { id: number }
interface Admin extends Base { role: "admin" }
type BaseT = { id: number };
type AdminT = BaseT & { role: "admin" };
Разница видна при конфликте свойств:
interface A { value: string }
interface B extends A { value: number }
// Ошибка сразу: Interface 'B' incorrectly extends interface 'A'
type C = { value: string } & { value: number };
// Ошибки нет, но value имеет тип never — проблема всплывёт позже
extends проверяет совместимость в момент объявления, пересечение & молча создаёт невозможный тип. Кроме того, документация TypeScript отмечает, что иерархии через interface extends компилятор обрабатывает эффективнее, чем большие пересечения.
Различие 4: сообщения об ошибках
В подсказках и ошибках interface обычно показывается по имени, а сложный type из пересечений и утилит может раскрываться в длинную структуру. В большом проекте это заметно влияет на читаемость ошибок. Разница не принципиальная, но приятная в пользу interface для объектов.
Что умеют оба
- Описывать объекты, методы, необязательные и
readonlyполя. - Использоваться в
implementsу классов (еслиtypeописывает объект, а не union). interfaceможет расширятьtype, аtype— пересекаться сinterface.- Поддерживать дженерики:
interface Box<T> { value: T }иtype Box<T> = { value: T }.
Сравнительная таблица
| Возможность | type | interface |
|---|---|---|
| Форма объекта | Да | Да |
| Union, кортежи, примитивы | Да | Нет |
| Mapped / conditional types | Да | Нет |
| Слияние объявлений | Нет | Да |
| Проверка конфликтов при наследовании | Нет (& даёт never) | Да (extends) |
| Имя в сообщениях об ошибках | Не всегда | Обычно да |
Простое правило для команды
Рабочий вариант, который легко соблюдать:
interface— для форм объектов: модели данных, пропсы компонентов, контракты API, публичные типы библиотеки.type— для union, кортежей, функций и всего, что собрано из утилит (Pick,Omit,Partial).- Наследование — через
extends, а не через&, если это возможно. - Слияние объявлений — только осознанно, для расширения внешних типов.
Альтернатива «везде type, interface только для расширения внешних типов» тоже нормальна. Важнее выбрать одно и закрепить его линтером: в typescript-eslint есть правило consistent-type-definitions.