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'.
В примере выше мы указали только метод sum
при реализации интерфейса классом Summator
. В результате код успешно скомпилировался, ведь метод multiply
был указан как опциональный. В то же время в экземпляре нашего класса мы не можем обратиться к этому методу.
Выводы
Поскольку в TypeScript для одних и тех же вещей существует несколько разных инструментов, мы можем реализовывать классы с помощью расширения абстрактных классов вместо интерфейсов. Но выбор будет зависеть от задачи. Абстрактные классы предоставляют нам модификаторы доступа и конструкторы, в то время как интерфейсы более легковесны и просты.
Задание
С помощью предоставленного интерфейса IPhonebook
и типа Entry
реализуйте класс Phonebook
, который представляет телефонный справочник со следующими свойствами:
entries
— база данных, объект, записи в котором представляют собой имена в качестве ключей и телефоны в качестве значений. Свойство должно быть неизменяемым и доступным только для чтенияget
— метод, возвращающий телефон по имениset
— метод, записывающий имя и телефон в справочник
Примеры:
typescript
const myNote = new Phonebook();
myNote.set('help', 911);
myNote.get('help'); // 911
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.