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
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.