Использование системы типов TypeScript и преодоление особенностей JavaScript для надежных проверок во время выполнения

вступление

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

У нас появились типы, автозаполнение, обнаружение проблем во время компиляции, и мир наконец-то успокоился, пока кто-то не решил использовать instanceof (да, это был я).

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

В этой статье я хочу погрузиться в тему объектов. В частности, различные способы создания экземпляра объекта, почему они важны и как оператор instanceof можно использовать/использовать не по назначению.

Я подкреплю все практическим примером обработки ошибок и покажу вам, как мы можем получить гибкость системы структурированной типизации машинописного текста с помощью проверки типов объектов во время выполнения.

Пример использования

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

  • ошибки аутентификации
  • серверные ошибки
  • любые неотловленные ошибки, не подпадающие под два предыдущих типа.

Давайте посмотрим, как мы будем реализовывать что-то для этого.

⚠️ Примечание. В следующих примерах я не расширяю базовый `Error` class для упрощения примера, но вы должны на 100% расширять `Error` class при создании пользовательских ошибок.

1. Что, если бы мы использовали только машинописный текст?

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

type AuthError = { reason: string };

type BackendError = { reason: string };

type UnknownError = { reason: string };

Прямо на пороге у нас есть несколько серьезных проблем:

  • Для машинописного текста это все одно и то же.
  • Для javascript их даже не существует
  • Повторение кода

Давайте перейдем к чему-то лучшему.

2. Что, если бы мы использовали классы?

Хорошая идея! В конце концов, классы также доступны во время выполнения, что решает вторую проблему.

class AuthError {
 constructor(public reason: string) {}
}

class BackendError {
 constructor(public reason: string) {}
}

class UnknownError {
 constructor(public reason: string) {}
}

У нас еще есть две проблемы. Это все то же самое для машинописного текста, и мы все еще повторяемся.

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

// Super class
class CustomError {
  constructor(public reason: string) {}
}

class AuthError extends CustomError {}

class BackendError extends CustomError {}

class UnknownError extends CustomError {}

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

function handleError(error: CustomError) {
  if (error instanceof AuthError) {
    // do something
  } else if (error instanceof BackendError) {
    // do something
  } else if (error instanceof UnknownError) {
    // do something
  }
}

Большой! Теперь мы позаботились обо всех проблемах, о которых упоминали ранее, но почему статья еще не закончена? О, о. Что-то вот-вот изменит ваше представление о чем-то в Javascript!

Проблема скрыта в том, как работает instanceof.

Проще говоря, someObject instanceof SomeClass возвращает true, если someObject был создан конструктором SomeClass или наследуется от него, а это означает, что где-то в них должно быть ключевое слово new. Подумаешь, ну и что? Мы просто выдадим наши ошибки следующим образом:

throw new AuthError('Session is expired or something');

Хорошо. достаточно справедливо, за исключением того, что это дикая необузданная земля javascript, где вы также можете сделать это:

throw { reason: 'Session is expired or something' };

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

const errorObj: AuthError = { reason: 'Session is expired or something' };

console.log(errorObj instanceof AuthError); // -> False

instanceof НЕ работает, если вы не создаете объекты с ключевым словом new.

Хотя это довольно маловероятно, лично я потерял веру в instanceof. Всякий раз, когда в этой области кода возникает проблема, тихий голос в моей голове будет продолжать говорить мне "Что, если это из-за той действительно редкой ошибки, о которой вы когда-то читали?"

Нам нужен другой подход. Тот, который спасает нас от причуд Javascript и позволяет машинописному тексту по-настоящему сиять.

3. Снова вернуться к типам

Представляем теги! Тег — это просто свойство с постоянным значением, которое объявлено в типе, что позволяет нам эффективно сузить тип данного объекта как во время компиляции, так и во время выполнения.

Вот как мы могли бы теперь определить наши ошибки:

type AuthError = {
  reason: string,
  errorType: 'auth-error', // <- tag
};

type BackendError = {
  reason: string,
  errorType: 'backend-error', // <- tag
};

type UnknownError = {
  reason: string,
  errorType: 'unknown-error', // <- tag
};

type CustomError = AuthError | BackendError | UnknownError;

Интересно, что CustomError теперь находится внизу нашей иерархии, а не наверху. Используя оператор | (объединение), мы сообщаем typescript, что CustomError является одним из любых заданных типов. Мы можем сузить, какой именно, используя тег.

Вот как можно переписать функцию handleError:

function handleError(error: CustomError) {
  switch (error.errorType) {
    case 'auth-error':
      // …
    case 'backend-error':
      // …
    default:
      // …
  }
}

Самая крутая часть? Typescript помогает вам на каждом этапе пути и полностью осознает тип ошибки, с которой вы работаете.

Обратите внимание, что для этого тег должен иметь одинаковое имя для всех типов (errorType в приведенном выше примере).

Если вам интересно, как печатать типы таким образом (используя // ^?), ознакомьтесь с этим расширением (я не аффилирован, просто думаю, что это круто).

Теперь даже не будет иметь значения, если мы будем использовать сыроедение и бросать объекты напрямую. Typescript взбесится и заставит нас добавить тег:

// ERROR: Property 'errorType' is missing in type
// '{ reason: string; }' but required in type 'AuthError'.
const errorObject: AuthError = {
  reason: string;
};

throw errorObject;

Заключение

В заключение в этой статье подчеркивается важность понимания систем типов Typescript и Javascript, объясненных на примере обработки ошибок.

Пошаговое исследование показало, что использование тегов — мощное и надежное решение. Этот подход использует преимущества структурированной системы типизации Typescript и позволяет выполнять проверки типов объектов во время выполнения, что приводит к более надежному и удобному в сопровождении коду.

Усвоив эти нюансы, мы сможем полностью использовать потенциал Typescript, избегая при этом языковых ловушек.

Чтобы лучше понять машинописный текст, я настоятельно рекомендую книгу Дэна Вандеркама «Эффективный машинописный текст».