Avatar ·

Можно ли расширять (extends) типы в TypeScript, как интерфейсы?

Всем привет! Я только начал изучать TypeScript и столкнулся с непониманием. Я знаю, что интерфейсы можно расширять через `extends`. Например: ```typescript interface User { name: string; } interface AdminUser extends User { role: string; } ``` Это работает отлично. Но когда я пытаюсь сделать то же самое с типами (type alias), у меня ничего не получается: ```typescript type User = { name: string; }; type AdminUser extends User { // ❌ Ошибка: 'extends' не работает с типами role: string; } ``` Я пробовал разные варианты, но TypeScript ругается. В интернете пишут, что типы можно расширять с помощью пересечений (&), но я не до конца понимаю, как это правильно делать и в чем разница между интерфейсами и типами в контексте расширения. Подскажите, пожалуйста, как правильно расширять типы? Можно ли вообще это делать? И если да, то когда лучше использовать `extends` для интерфейсов, а когда пересечения для типов? Заранее спасибо за помощь! <<>>

Да, типы можно "расширять" — но не через 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 — это про читаемость и поддерживаемость кода, а не про строгое следование одному стилю.