$ sudo teach IT
МОДУЛЬ 5 · УРОК 4

Расширение интерфейсов (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 плюс свои собственные.

Использование расширенных интерфейсов

Объекты, соответствующие расширенному интерфейсу, должны содержать все свойства — и унаследованные, и собственные:

const user: User = {
  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
};

Этот подход позволяет создавать гибкие, переиспользуемые наборы свойств и комбинировать их по необходимости. Вы создаёте маленькие интерфейсы для разных аспектов и комбинируете их для создания сложных структур.

Цепочки наследования

Интерфейсы могут образовывать цепочки наследования:

// Уровень 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
};

Цепочки наследования позволяют создавать иерархии типов, отражающие реальную структуру данных. Однако не стоит создавать слишком длинные цепочки — это может усложнить код.

Переопределение свойств

При наследовании можно переопределять типы свойств, но с ограничениями:

interface Container {
  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").

Практический пример: система уведомлений

Рассмотрим реальный пример иерархии интерфейсов для системы уведомлений:

interface BaseNotification {
  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) и пересечение (&). Вот их различия:

// Extends — наследование
interface UserA extends Entity {
  name: string;
}

// Intersection type — пересечение
type UserB = Entity & {
  name: string;
};
Extends (наследование):
  • Создаёт иерархию типов
  • Поддерживает переопределение свойств
  • Более читаемо для OOP-паттернов
  • Лучше для классов (implements)
Intersection (&):
  • Комбинирует типы без иерархии
  • Нельзя переопределять свойства
  • Лучше для ad-hoc комбинаций
  • Работает с любыми типами

Используйте extends для создания иерархий типов и & для быстрой комбинации типов. Для интерфейсов обычно предпочтительнее extends, так как он создаёт более чёткую структуру.

Распространённые ошибки

Ошибка: конфликт свойств
interface A { x: string; }
interface B { x: number; }
interface C extends A, B {}
// Ошибка: конфликт типов
Правильно: разные имена
interface A { x: string; }
interface B { y: number; }
interface C extends A, B {}
// OK: разные свойства
Ошибка: забыли свойства
interface Entity { id: number; }
interface User extends Entity {}
const u: User = { name: "A" };
// Ошибка: нет id!
Правильно: все свойства
interface Entity { id: number; }
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-системы:

// 1. Создайте базовый интерфейс Product с общими свойствами
// (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 вопросов

Расширение интерфейсов

Premium