Проверка типа во время выполнения TypeScript

Apr 29 2023
Использование системы типов TypeScript и преодоление особенностей JavaScript для надежных проверок во время выполнения
Введение Typescript — это лучшее, что случилось с Javascript с тех пор, как рухнула Великая пирамида обратного вызова. У нас появились типы, автозаполнение, отлов ошибок во время компиляции, и мир наконец-то успокоился, пока кто-то не решил использовать instanceof (да, это был я).

вступление

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

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

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

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

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

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

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

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

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

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 {}


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

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

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

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

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

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

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

Несмотря на то, что это довольно маловероятная проблема, лично я потерял веру в 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;

Вот как 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, избегая при этом языковых ловушек.

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