Можно ли расширять (наследовать) типы в TypeScript?
Основные способы расширения типов в 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.