Можно ли расширять (extends) типы в TypeScript, как интерфейсы?
Да, типы можно "расширять" — но не через extends, а через пересечения (&)
В TypeScript существует два основных способа создания составных типов: интерфейсы (с ключевым словом interface) и псевдонимы типов (с ключевым словом type). У них разный синтаксис для расширения:
- Интерфейсы используют
extends(как в вашем примере). - Типы используют оператор пересечения
&(амперсанд).
Как правильно расширять тип
Вот корректный способ расширить тип User до AdminUser:
type User = {
name: string;
};
type AdminUser = User & {
role: string;
};
// Теперь AdminUser содержит оба поля:
const admin: AdminUser = {
name: "Alice",
role: "superadmin"
};
Оператор & создает пересечение (intersection) двух типов — новый тип, который включает все свойства из обоих исходных типов. Это работает для любых типов, не только для объектных:
type HasId = { id: number };
type HasName = { name: string };
type Person = HasId & HasName; // { id: number; name: string }
// Можно комбинировать с примитивами и другими типами:
type A = { x: number };
type B = { y: string };
type C = A & B; // { x: number; y: string }
Можно ли "расширять" типы через другие типы?
Да, вы можете строить цепочки:
type Base = { created: Date };
type WithName = Base & { name: string };
type WithAge = WithName & { age: number };
// Итоговый тип: { created: Date; name: string; age: number }
Когда использовать interface extends, а когда type &?
Вот ключевые различия:
| Критерий | Interface extends | Type & |
|---|---|---|
| Повторное объявление | Можно (declaration merging) | Нельзя (ошибка дублирования) |
| Работа с примитивами | Только объекты | Любые типы (union, tuple, примитивы) |
| Производительность | Быстрее (TypeScript оптимизирует) | Может быть медленнее при сложных пересечениях |
| Наследование от нескольких | extends A, B, C |
A & B & C |
Практический совет
Используйте интерфейсы когда:
- Вам нужно расширять типы из библиотек (declaration merging).
- Вы пишете публичный API или библиотеку.
- Вам нужна максимальная производительность компиляции.
Используйте типы с пересечениями когда:
- Вам нужно комбинировать не-объектные типы (например,
string & { brand: 'email' }). - Вы работаете с условными типами, маппингом или утилитами типа
Pick/Omit. - Вам нужно создать точную копию другого типа с изменениями (через
Omit+&).
Важный нюанс: конфликты свойств
При пересечении типов с одинаковыми именами свойств, но разными типами, TypeScript может создать never (если типы несовместимы):
type A = { x: number };
type B = { x: string };
type C = A & B; // x: never (невозможно одновременно number и string)
Интерфейсы с extends в такой ситуации выдадут ошибку компиляции, что обычно удобнее.
Альтернативный подход: использование утилитарных типов
Если вам нужно "расширить" тип, но вы хотите сохранить читаемость и избежать глубоких пересечений, можно использовать встроенные утилитарные типы TypeScript, такие как Pick, Omit, Partial и другие.
Например, вместо:
type User = { name: string; email: string; age: number };
type UserWithRole = User & { role: string };
Можно сделать так:
type User = { name: string; email: string; age: number };
// Создаем новый тип на основе User, но добавляем role
type UserWithRole = Omit<User, 'age'> & { role: string; age?: number };
// Или если нужно взять только часть свойств:
type UserPreview = Pick<User, 'name' | 'email'> & { isActive: boolean };
Этот подход особенно полезен, когда вы хотите:
- Исключить некоторые поля из исходного типа.
- Изменить тип существующего поля (сделать опциональным, например).
- Создать вариант типа с дополнительными ограничениями.
Также можно комбинировать оба подхода: использовать interface extends для базовой иерархии, а type с утилитами для более сложных трансформаций. Главное — помнить, что TypeScript — это про читаемость и поддерживаемость кода, а не про строгое следование одному стилю.