Бесплатный курс по TypeScript. Зарегистрируйтесь для отслеживания прогресса →

TypeScript: Реализация интерфейсов классами

В TypeScript классы могут тесно взаимодействовать с интерфейсами. В прошлом уроке мы увидели как интерфейсы могут расширять другие интерфейсы и комбинировать их. Аналогичным образом классы могут быть расширены интерфейсами:

interface IBeep {
  sayBeep: () => string;
}

interface IBoop {
  sayBoop: () => string;
}

class Robo implements IBeep, IBoop {
  sayBeep = () => 'beep';
  sayBoop = () => 'boop';
}

const R2D2 = new Robo();
R2D2.sayBeep(); // 'beep'

Мы можем создавать классы на основе интерфейсов так же, как мы создаем интерфейсы на основе интерфейсов. Но есть и отличия.

Если мы создаем интерфейс или тип и потом транспилируем TypeScript в JavaScript, то в коде не останется образца этого интерфейса. В то же время при создании класса его образец всегда переносится и в JavaScript при транспиляции.

Так можно сказать, что вариант с интерфейсами более легковесный, но все же выбор должен зависеть от задачи, которую мы решаем.

Создание класса на основе интерфейса не ведет к реализации этого интерфейса в классе. TypeScript просто проверяет, удовлетворяют ли свойства и методы нашего класса свойствам, заявленным в интерфейсе. Сам же класс мы пишем вручную. Рассмотрим пример:

interface ICalculate {
  sum: (num1: number, num2: number ) => number;
}

class Summator implements ICalculate {
  sum(num1, num2) { return num1 + num2; }
  // Для параметров будет выведено сообщение: Parameter 'num1'/'num2' implicitly has an 'any' type,
  // потому что TypeScript только проверяет класс на соответствие интерфейсу, но не наследуется от него полноценно

  multiply(num1: number, num2: number) { return num1 * num2; }
  // Мы добавили новый метод, но TypeScript не ругается
}

let calculator = new Summator();
  // Наш код сработает, как если бы он сработал для аргументов с типом any,
  // потому что типы параметров, равно как и все остальное, не были унаследованы классом при реализации интерфейса
calculator.sum(2,3) // 5

Ошибка в реализации интерфейса классом возможна только тогда, когда мы не реализуем одно из свойств, указанных в интерфейсе. Или мы реализуем его не так, как указано в интерфейсе:

interface ICalculate {
  sum: (num1: number, num2: number ) => number;
}

class Summator implements ICalculate {
  sum (num1: string, num2: string) { return num1 + num2 };
  // Мы изменили типы аргументов на string, то есть неверно реализовали интерфейс
  // В таком случае TypeScript обратит внимание на нашу ошибку и не скопилируется:
  // Type '(num1: string, num2: string) => string' is not assignable to type '(num1: number, num2: number) => number'.
}

По этой же причине, если мы пишем класс, реализующий интерфейс с опциональными свойствами, нам нужно прописывать все самостоятельно. В противном случае эти свойства не попадут в наш класс:

interface ICalculate {
  sum: (num1: number, num2: number ) => number;
  multiply? : (num1: number, num2: number ) => number;
}

class Summator implements ICalculate {
  sum (num1: number, num2: number) { return num1 + num2; }
}

const calculator = new Summator();
calculator.sum(2,3) // 5
calculator.multiply(2,3) // Property 'multiply' does not exist on type 'Summator'.

Поскольку в TypeScript для одних и тех же вещей существует несколько разных инструментов, мы можем реализовывать классы с помощью расширения абстрактных классов вместо интерфейсов. Но выбор будет зависеть от задачи. Абстрактные классы предоставляют нам модификаторы доступа и конструкторы, в то время как интерфейсы более легковесны и просты.

Задание

С помощью предоставленного интерфейса IPhonebook и типа Entry реализуйте и экспортируйте по умолчанию класс Phonebook, который представляет телефонный справочник со следующими свойствами:

  • entries — база данных, объект, записи в котором представляют собой имена в качестве ключей и телефоны в качестве значений. Свойство должно быть неизменяемым и доступным только для чтения
  • get — метод, возвращающий телефон по имени
  • set — метод, записывающий имя и телефон в справочник

Примеры:
typescript
const myNote = new Phonebook();
myNote.set('help', 911);
myNote.get('help'); // 911

Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе