Расширение интерфейсов (extends)
Наследование интерфейсов и их композиция
Цели урока
- Понять концепцию наследования интерфейсов
- Освоить синтаксис extends
- Научиться расширять несколько интерфейсов
- Понять, когда использовать наследование vs композицию
- Изучить цепочки наследования и их ограничения
Зачем нужно расширение интерфейсов?
При разработке приложений часто возникают ситуации, когда нужно описать объекты, которые имеют общие свойства, но отличаются дополнительными полями. Например, все сущности в базе данных имеют id и createdAt, но каждая конкретная сущность (пользователь, товар, заказ) имеет свои уникальные свойства.
Без расширения интерфейсов вам пришлось бы дублировать общие свойства в каждом интерфейсе. Это приводит к нарушению принципа DRY (Don't Repeat Yourself) и затрудняет поддержку кода. Если вы решите изменить структуру общих свойств, придётся менять это во всех местах.
Наследование интерфейсов решает эту проблему, позволяя создавать базовые интерфейсы с общими свойствами и расширять их для конкретных случаев. Это делает код более структурированным и легко поддерживаемым.
Синтаксис extends
Ключевое слово extends позволяет интерфейсу наследовать свойства от другого интерфейса:
interface Entity {
readonly id: number;
readonly createdAt: string;
}
// Расширенный интерфейс для пользователей
interface User extends Entity {
name: string;
email: string;
role: "admin" | "user";
}
// Ещё один расширенный интерфейс для товаров
interface Product extends Entity {
name: string;
price: number;
category: string;
}
Интерфейс User наследует свойства id и createdAt от Entity и добавляет собственные свойства. Теперь User содержит все свойства Entity плюс свои собственные.
Использование расширенных интерфейсов
Объекты, соответствующие расширенному интерфейсу, должны содержать все свойства — и унаследованные, и собственные:
id: 1, // из Entity
createdAt: "2024-01-15", // из Entity
name: "Алексей", // из User
email: "alex@example.com",
role: "admin"
};
const product: Product = {
id: 101, // из Entity
createdAt: "2024-02-20", // из Entity
name: "Ноутбук", // из Product
price: 75000,
category: "Электроника"
};
// TypeScript проверяет наличие всех свойств:
// const badUser: User = { name: "Test" };
// Ошибка! Отсутствуют id, createdAt, email, role
Множественное наследование
TypeScript позволяет наследовать от нескольких интерфейсов одновременно, разделяя их запятой:
interface HasId {
readonly id: number;
}
interface HasTimestamps {
readonly createdAt: string;
updatedAt: string;
}
interface HasSoftDelete {
deletedAt?: string;
}
interface HasAudit {
createdBy: number;
updatedBy?: number;
}
// Множественное наследование
interface BaseEntity extends HasId, HasTimestamps, HasSoftDelete, HasAudit {
name: string;
}
// Теперь BaseEntity содержит ВСЕ свойства:
// - id (из HasId)
// - createdAt, updatedAt (из HasTimestamps)
// - deletedAt (из HasSoftDelete)
// - createdBy, updatedBy (из HasAudit)
// - name (собственное)
// Использование:
const entity: BaseEntity = {
id: 1,
name: "Тестовая сущность",
createdAt: "2024-01-15",
updatedAt: "2024-01-15",
createdBy: 1
};
Этот подход позволяет создавать гибкие, переиспользуемые наборы свойств и комбинировать их по необходимости. Вы создаёте маленькие интерфейсы для разных аспектов и комбинируете их для создания сложных структур.
Цепочки наследования
Интерфейсы могут образовывать цепочки наследования:
interface Animal {
name: string;
age: number;
}
// Уровень 2: Расширение базового
interface Pet extends Animal {
owner: string;
vaccinated: boolean;
}
// Уровень 3: Конкретный тип
interface Dog extends Pet {
breed: string;
barkVolume: number;
}
// Dog содержит ВСЕ свойства:
// - name, age (из Animal)
// - owner, vaccinated (из Pet)
// - breed, barkVolume (собственные)
const myDog: Dog = {
name: "Шарик",
age: 5,
owner: "Пригов",
vaccinated: true,
breed: "Лабрадор",
barkVolume: 8
};
Цепочки наследования позволяют создавать иерархии типов, отражающие реальную структуру данных. Однако не стоит создавать слишком длинные цепочки — это может усложнить код.
Переопределение свойств
При наследовании можно переопределять типы свойств, но с ограничениями:
value: string;
}
// Можно сузить тип (string → "hello")
interface SpecificContainer extends Container {
value: "hello";
}
// Можно расширить тип (string → string | number)
interface FlexibleContainer extends Container {
value: string | number;
}
// Можно добавить readonly
interface ImmutableContainer extends Container {
readonly value: string;
}
// НЕЛЬЗЯ: изменить тип на несовместимый
// interface Bad extends Container {
// value: number; // Ошибка!
// }
Переопределяемое свойство должно быть совместимым с типом родительского свойства. Нельзя сделать тип строже (например, string → number), но можно расширить (string → string | number) или сузить литералами (string → "hello").
Практический пример: система уведомлений
Рассмотрим реальный пример иерархии интерфейсов для системы уведомлений:
readonly id: string;
title: string;
message: string;
createdAt: Date;
read: boolean;
}
interface EmailNotification extends BaseNotification {
type: "email";
recipient: string;
subject: string;
}
interface PushNotification extends BaseNotification {
type: "push";
deviceId: string;
badge?: number;
}
interface SmsNotification extends BaseNotification {
type: "sms";
phoneNumber: string;
}
// Union тип для обработки любого уведомления
type Notification = EmailNotification | PushNotification | SmsNotification;
// Функция-обработчик с типизацией
function sendNotification(notification: Notification): void {
switch (notification.type) {
case "email":
console.log(`Email to ${notification.recipient}: ${notification.subject}`);
break;
case "push":
console.log(`Push to ${notification.deviceId}: ${notification.title}`);
break;
case "sms":
console.log(`SMS to ${notification.phoneNumber}: ${notification.message}`);
break;
}
}
Эта иерархия позволяет типобезопасно обрабатывать разные типы уведомлений, сохраняя общие свойства в базовом интерфейсе. Добавление нового типа уведомления не потребует изменения существующего кода обработки.
Extends vs Intersection Types
TypeScript предлагает два способа комбинировать типы: наследование (extends) и пересечение (&). Вот их различия:
interface UserA extends Entity {
name: string;
}
// Intersection type — пересечение
type UserB = Entity & {
name: string;
};
- Создаёт иерархию типов
- Поддерживает переопределение свойств
- Более читаемо для OOP-паттернов
- Лучше для классов (implements)
- Комбинирует типы без иерархии
- Нельзя переопределять свойства
- Лучше для ad-hoc комбинаций
- Работает с любыми типами
Используйте extends для создания иерархий типов и & для быстрой комбинации типов. Для интерфейсов обычно предпочтительнее extends, так как он создаёт более чёткую структуру.
Распространённые ошибки
interface B { x: number; }
interface C extends A, B {}
// Ошибка: конфликт типов
interface B { y: number; }
interface C extends A, B {}
// OK: разные свойства
interface User extends Entity {}
const u: User = { name: "A" };
// Ошибка: нет id!
interface User extends Entity { name: string; }
const u: User = { id: 1, name: "A" };
// OK!
Паттерн: полиморфизм через интерфейсы
Наследование интерфейсов позволяет реализовать полиморфизм — одна функция может работать с разными типами данных:
interface Shape {
area(): number;
perimeter(): number;
type: string;
}
// Конкретные реализации
interface Circle extends Shape {
type: "circle";
radius: number;
}
interface Rectangle extends Shape {
type: "rectangle";
width: number;
height: number;
}
// Функция работает с любым Shape
function describeShape(shape: Shape): string {
return `Фигура: ${shape.type}, площадь: ${shape.area()}, периметр: ${shape.perimeter()}`;
}
// Функция сужает тип через switch
function printDetails(shape: Circle | Rectangle) {
switch (shape.type) {
case "circle":
console.log(`Круг с радиусом ${shape.radius}`);
break;
case "rectangle":
console.log(`Прямоугольник ${shape.width}x${shape.height}`);
break;
}
}
Полиморфизм позволяет писать гибкий код, который работает с любыми типами, соответствующими интерфейсу. Добавление нового типа не потребует изменения существующих функций.
Смешанное наследование
TypeScript позволяет комбинировать extends с дополнительными свойствами:
interface BaseEntity {
id: number;
}
// Расширение с дополнительными свойствами
interface TimestampedEntity extends BaseEntity {
createdAt: Date;
updatedAt: Date;
}
// Ещё одно расширение
interface SoftDeletable extends TimestampedEntity {
deletedAt?: Date;
isDeleted: boolean;
}
// Конкретный тип
interface User extends SoftDeletable {
name: string;
email: string;
}
Такой подход позволяет создавать гибкие иерархии типов с минимальным дублированием кода.
Практическое задание
Создайте иерархию интерфейсов для ecommerce-системы:
// (id, name, price, description)
// 2. Создайте интерфейс PhysicalProduct (extends Product)
// с полями: weight, dimensions, inStock
// 3. Создайте интерфейс DigitalProduct (extends Product)
// с полями: fileSize, downloadUrl, licenseKey
// 4. Создайте union-тип Product = PhysicalProduct | DigitalProduct
// 5. Напишите функцию-обработчик для каждого типа товара
Чек-лист урока
- Extends — ключевое слово для наследования интерфейсов
- Синтаксис:
interface Child extends Parent { ... } - Можно наследовать от нескольких интерфейсов:
extends A, B, C - Поддерживаются цепочки наследования
- Переопределяемые свойства должны быть совместимы по типу
- Extends создаёт иерархии типов, & — быстрые комбинации
- Используйте extends для DRY и создания иерархий типов
- Избегайте слишком длинных цепочек наследования
Тест по расширению интерфейсов
8 вопросов