Создание повторно используемых базовых классов в TypeScript с реальным примером

В этой статье мы расскажем о создании повторно используемых базовых классов в TypeScript.

Цели базового класса

Базовый класс предлагает структуру и общий код, который другие классы могут расширить. Вот пример базового класса TypeScript, который мы расширим чуть позже:

abstract class Automobile {
  private _speed: number = 0;
  private _direction: number = 0;
  public drive(speed: number): void {
    this._speed = speed;
  }
  
  public turn(direction: number): void {
    if (direction > 90 || direction < -90) {
      throw new Error(`Invalid direction "${direction}"`);
    }
    this._direction = direction;
  }
  
  public abstract getNumDoors(): number;
}

Мы создали базовый класс Automobile. Он предоставляет некоторую базовую функциональность для всех типов автомобилей: легковой, пикап и т.д. Обратите внимание, что этот класс объявлен как абстрактный. Он включает в себя метод getNumDoors. Его реализация зависит от конкретного класса автомобилей.

Определив абстрактный класс, мы показываем, что необходим еще один класс для его расширения. Пример:

class Sedan extends Automobile {
  public getNumDoors(): number {
    return 4;
  }
}

const myCar = new Sedan();
myCar.getNumDoors(); // выведет 4
myCar.drive(35);     // зададим для speed значение 35
myCar.turn(20);      // зададим для direction значение 20

Мы расширяем класс Automobile, чтобы создать класс четырехдверных седанов. Теперь мы можем вызывать методы обоих классов, так как если бы они были одним классом. Для данного проекта мы создадим базовый класс с именем AbstractIO.

Создание базового класса Abstract IO

Abstract IO является базовым классом, который я создал для реализации спецификаций IO Plugin всеми его авторами. Теперь он доступен на npm.

При этом создаваемый базовый класс должен соответствовать лучшим практикам разработки на TypeScript и JavaScript. Чтобы проиллюстрировать это, рассмотрим свойство MODES и метод digitalWrite. Они оба необходимы для работы IO Plugins.

Свойство MODES является объектом. Он содержит названия режимов и их числовые значения. Они используются в API.

В TypeScript мы хотели бы использовать перечисления (enum)  чтобы представить эти значения. Но JavaScript их не поддерживает. В этом языке для определения последовательности констант применяется объект-контейнер.

Чтобы решить эту дилемму, создадим подход, использующий другой. Начнем с enum в TypeScript. Инициализируем каждый режим определенным значением:

export enum Mode {
 INPUT = 0,
 OUTPUT = 1,
 ANALOG = 2,
 PWM = 3,
 SERVO = 4,
 STEPPER = 5,
 UNKNOWN = 99
}

Каждое из них соответствует значению в свойстве MODES. Это определено в спецификациях, которые реализуются как часть класса Abstract IO:

public get MODES() {
 return {
   INPUT: Mode.INPUT,
   OUTPUT: Mode.OUTPUT,
   ANALOG: Mode.ANALOG,
   PWM: Mode.PWM,
   SERVO: Mode.SERVO,
   UNKNOWN: Mode.UNKNOWN
 }
}

Благодаря этому мы получили перечисления в TypeScript и можем полностью игнорировать числовые значения. В JavaScript у нас есть свойство MODES, поэтому нет необходимости напрямую использовать числовые значения.

Но что на счет методов в базовом классе, которые должны переопределяться  в производных? В TypeScript для этого придется использовать абстрактный класс. Но с их поддержкой в JS дела обстоят туго.

Я не смог предложить способ для обоих языков, поэтому  по умолчанию использую подход JavaScript. Для этого я создал в базовом классе метод, который выдает исключение:

public digitalWrite(pin: string | number, value: number): void {
 throw new Error(`digitalWrite is not supported by ${this.name}`);
}

Выводы

Чтобы решить несоответствия в TypeScript и JavaScript, нужно:

  1. Использовать практику, приемлемую для каждого языка программирования;
  2. Использовать подходы TypeScript и сопоставить им аналоги в JavaScript, как мы сделали для свойства;
  3. Использовать подход JavaScript, как мы сделали для метода digitalWrite. и при этом игнорируя лучшие практики TypeScript.

Я обнаружил, что абстрактные классы бесполезны в проектах, созданных на чистом JavaScript. Но TypeScript также не предоставляет механизмы для обеспечения безопасности типов при использовании механизма переопределения. Такого как override, ключевое слово в C#.

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

Заключение

Создание базовых классов, предназначенных для пользователей TypeScript и чистого JavaScript, является наиболее простым процессом. Но тут есть подводные камни.

Мне удалось достигнуть намеченных целей с Abstract IO. А также реализовать базовый класс, который устраивает авторов IO Plugin на TypeScript и чистом JavaScript. Как оказалось, можно погнаться за двумя зайцами и поймать обоих!

Наталья Кайдаавтор-переводчик статьи «Creating Reusable Base Classes in TypeScript with a Real-Life Example»