Avatar ·

extends и implements в TypeScript: в чем разница?

Всем привет! Я только начал изучать TypeScript после JavaScript и запутался в двух ключевых словах — `extends` и `implements`. Вроде оба используются для работы с классами и интерфейсами, но я не понимаю, когда что применять. У меня есть простой интерфейс `Animal`, который описывает базовые свойства и методы: ```typescript interface Animal { name: string; makeSound(): void; } ``` И я хочу создать класс `Dog`. В примерах я видел, как используют и `extends`, и `implements`: ```typescript // Вариант 1: implements class Dog implements Animal { name: string; constructor(name: string) { this.name = name; } makeSound() { console.log("Woof!"); } } // Вариант 2: extends (если бы был базовый класс) class AnimalBase { name: string; constructor(name: string) { this.name = name; } } class Dog extends AnimalBase { makeSound() { console.log("Woof!"); } } ``` Но я не понимаю, в чем принципиальная разница. Когда использовать `extends`, а когда `implements`? Можно ли их комбинировать? И что происходит с наследованием реализации — `implements` ведь не наследует код, а только проверяет структуру? Буду благодарен за подробное объяснение с примерами! <<>>

Ключевая разница: наследование реализации vs. следование контракту

Давайте разберем фундаментальное отличие, которое сразу всё прояснит:

  • extends — это наследование. Вы берете готовый класс-родитель и получаете от него всю его реализацию (методы, свойства, логику). Вы можете переопределить (override) методы родителя или добавить свои.
  • implements — это соблюдение контракта. Вы говорите TypeScript: "Этот класс обязуется иметь такую же структуру, как указанный интерфейс". Но никакой реализации вы не получаете — вы должны написать её сами.

Пример, показывающий разницу на практике

// Интерфейс — только описание структуры
interface Flyable {
  fly(): void;
  altitude: number;
}

// Класс с готовой реализацией
class Bird {
  public name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  public eat(): void {
    console.log(`${this.name} is eating`);
  }
}

// Вариант 1: implements — берём только "форму", пишем всё сами
class Airplane implements Flyable {
  altitude: number = 0;
  
  fly(): void {
    console.log("Airplane is flying using engines");
  }
}

// Вариант 2: extends — берём готовую реализацию + можем добавить
class Sparrow extends Bird implements Flyable {
  altitude: number = 0;
  
  constructor(name: string) {
    super(name); // обязательно вызываем конструктор родителя
  }
  
  fly(): void {
    console.log(`${this.name} is flying with wings`);
  }
  
  // Метод eat() уже есть от Bird, его не нужно писать заново
}

// Использование
const plane = new Airplane();
plane.fly(); // "Airplane is flying using engines"

const sparrow = new Sparrow("Jack");
sparrow.eat(); // "Jack is eating" — метод унаследован от Bird
sparrow.fly(); // "Jack is flying with wings" — своя реализация

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

  • extends — когда у вас есть готовый класс с логикой, которую вы хотите переиспользовать. Например, базовый класс Vehicle с методами start(), stop(), и вы создаете Car extends Vehicle.
  • implements — когда вы хотите гарантировать, что класс соответствует определённой структуре (интерфейсу), но реализация может быть совершенно разной. Например, интерфейс Serializable с методом toJSON() — разные классы могут сериализоваться по-разному.

Комбинирование extends и implements

Вы можете использовать оба ключевых слова одновременно! Это очень мощный паттерн:

interface IEmployee {
  salary: number;
  work(): void;
}

class Person {
  name: string;
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  introduce(): void {
    console.log(`Hi, I'm ${this.name}`);
  }
}

// Наследуем реализацию Person + соблюдаем контракт IEmployee
class Developer extends Person implements IEmployee {
  salary: number;
  
  constructor(name: string, age: number, salary: number) {
    super(name, age); // вызываем конструктор Person
    this.salary = salary;
  }
  
  work(): void {
    console.log(`${this.name} is writing code`);
  }
  
  // introduce() уже есть от Person
}

Важное замечание про интерфейсы

Интерфейсы тоже могут расширять другие интерфейсы (через extends):

interface BasicAnimal {
  name: string;
  makeSound(): void;
}

interface Pet extends BasicAnimal {
  owner: string;
  play(): void;
}

// Класс должен реализовать ВСЕ поля из BasicAnimal и Pet
class Cat implements Pet {
  name: string;
  owner: string;
  
  constructor(name: string, owner: string) {
    this.name = name;
    this.owner = owner;
  }
  
  makeSound(): void {
    console.log("Meow!");
  }
  
  play(): void {
    console.log(`${this.name} is playing`);
  }
}

Итог: extends = "я беру твою реализацию", implements = "я обязуюсь иметь такую же форму". Используйте их осознанно, и код станет чище и понятнее.

<<>>

Дополнение: extends для интерфейсов и абстрактные классы

Стоит упомянуть, что extends может использоваться не только с классами, но и с интерфейсами. Это позволяет создавать иерархии типов:

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

// Теперь Square включает color + sideLength

Также есть третий вариант — абстрактные классы (abstract class). Они занимают промежуточное положение:

  • Могут содержать как реализованные методы (как обычный класс), так и абстрактные (без реализации, как интерфейс).
  • Используются с extends, но сами по себе не могут быть инстанциированы.
abstract class Vehicle {
  abstract start(): void; // без реализации
  
  stop(): void { // с реализацией
    console.log("Vehicle stopped");
  }
}

class Car extends Vehicle {
  start(): void { // обязаны реализовать
    console.log("Car started");
  }
  // stop() уже есть
}

Выбор между интерфейсом и абстрактным классом — вопрос дизайна. Если вам нужно только описание структуры — используйте interface. Если нужна частичная реализация — abstract class.