Avatar ·

Можно ли расширять (наследовать) типы в TypeScript?

Всем привет! Я только начал изучать TypeScript и столкнулся с проблемой. У меня есть интерфейс для базовых данных пользователя: ```typescript interface User { id: number; name: string; email: string; } ``` Теперь мне нужно создать тип для администратора, у которого есть все поля обычного пользователя, но ещё и роль (role) и уровень доступа (permissions). Я знаю, что интерфейсы можно расширять через `extends`, но работает ли это для типов, объявленных через `type`? Я пробовал сделать так: ```typescript type User = { id: number; name: string; email: string; } type Admin = User & { role: string; permissions: string[]; } ``` Это сработало, но мне сказали, что это не совсем «расширение», а пересечение типов. В чём разница? И есть ли способ сделать классическое наследование для типов, как в интерфейсах? Заранее спасибо за помощь! <<>>

Основные способы расширения типов в TypeScript

Да, типы в TypeScript можно расширять, но механика отличается от интерфейсов. Есть два основных подхода: пересечение типов (intersection types) и расширение через интерфейсы. Рассмотрим оба.

1. Пересечение типов (Intersection Types) — ваш вариант

То, что вы написали — это абсолютно правильный и распространённый способ. Оператор & создаёт новый тип, который объединяет все свойства обоих типов:

type User = {
  id: number;
  name: string;
  email: string;
}

type Admin = User & {
  role: string;
  permissions: string[];
}

// Теперь Admin имеет все поля User + свои
const admin: Admin = {
  id: 1,
  name: "Иван",
  email: "ivan@example.com",
  role: "superadmin",
  permissions: ["read", "write", "delete"]
};

Этот подход полностью типобезопасен и широко используется в продакшн-коде. Он особенно удобен, когда нужно комбинировать несколько независимых типов.

2. Расширение через интерфейсы (Interface extends)

Если вы предпочитаете классическое наследование, используйте интерфейсы. Они поддерживают extends как в объектно-ориентированном программировании:

interface User {
  id: number;
  name: string;
  email: string;
}

interface Admin extends User {
  role: string;
  permissions: string[];
}

// Работает аналогично
const admin: Admin = {
  id: 2,
  name: "Мария",
  email: "maria@example.com",
  role: "moderator",
  permissions: ["read", "write"]
};

В чём разница между type и interface?

  • Пересечение типов (&) — это композиция. Вы можете объединять любые типы: примитивные, литеральные, union-типы. Недостаток: если два типа имеют одинаковое свойство с разными типами, TypeScript может создать never (невозможный тип).
  • Интерфейсы с extends — это наследование. Работает только с объектными типами. При конфликте свойств выдаёт ошибку компиляции, а не создаёт never.

Когда что использовать?

  • Используйте интерфейсы, если: вам нужно классическое ООП-наследование, вы работаете с классами, или вы пишете библиотеку — интерфейсы лучше расширяются пользователями.
  • Используйте типы с &, если: вам нужно объединить union-типы, добавить свойства к примитивам, или создать сложную композицию из нескольких типов.

Дополнительный приём: условное расширение

С типами можно делать более гибкие вещи, например, расширять тип на основе условий:

type WithRole<T> = T & { role: string };

type BasicUser = { name: string };
type Admin = WithRole<BasicUser>; // { name: string; role: string }

В итоге: ваш пример с & — это полноценное расширение типа. Не верьте тем, кто говорит, что это "не совсем расширение". Это просто другой синтаксис, который даёт больше гибкости.

<<>>

Альтернативный подход: использование дженериков для расширения

Если вам нужно создавать множество вариаций одного базового типа, удобно использовать дженерики (generics) в сочетании с пересечением типов. Это позволяет избежать дублирования кода:

type User<TAdditional = {}> = {
  id: number;
  name: string;
  email: string;
} & TAdditional;

// Создаём конкретные типы
type Admin = User<{ role: string; permissions: string[] }>;
type Moderator = User<{ section: string; canBan: boolean }>;

// Использование
const admin: Admin = {
  id: 1,
  name: "Пётр",
  email: "petr@example.com",
  role: "admin",
  permissions: ["all"]
};

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

Ещё один вариант — использовать типы-утилиты вроде Partial, Pick или Omit для создания производных типов без явного расширения:

type User = { id: number; name: string; email: string; role?: string };
type Admin = Required<Pick<User, 'id' | 'name' | 'email'>> & { role: string; permissions: string[] };

Выбирайте подход под конкретную задачу — все они корректны с точки зрения TypeScript.