Заметки о продвинутом TypeScript: Удостоверения во время выполнения


Введение

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

Примечание: Эта заметка является обновленной версией оригинальной заметки о TypeScript: Обработка побочных эффектов

Базовый

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

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

const loadUser = (id: number) => {
  fetch(`http://www.your-defined-endpoint.com/users/${id}`)
    .then((response) => response.json())
    .then((user: User) => saveUser(user))
    .catch((error) => {
      console.log({ error });
    });
};
Войти в полноэкранный режим Выйти из полноэкранного режима

На первый взгляд все это звучит разумно, мы получаем пользователя по id, а затем сохраняем данные для дальнейшей обработки. Если вы посмотрите на код внимательнее, то заметите, что после декодирования json-данных мы определили тип данных User. Тип User в этом примере определяется следующим образом:

type User = {
  id: number;
  name: string;
  active: boolean;
  profile: {
    activatedAt: number;
  };
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Интересно, что код скомпилируется и TypeScript не покажет никаких ошибок, поскольку мы определили User и заявили, что ответ, после декодирования, всегда будет вышеупомянутого типа. Еще более интересным является тот факт, что вызов функции json на объекте ответа возвращает Promise<any>, поэтому нет фактической гарантии, что мы имеем дело с типом User во время выполнения.

Рассмотрим сценарий, в котором наши предположения могут не сработать, поэтому добавим функцию saveUser, которая ожидает пользователя с некоторой информацией о профиле:

const saveUser = (user: User) => {
  const activationDate = user.profile.activatedAt;
  // do something with the information...
};
Войти в полноэкранный режим Выход из полноэкранного режима

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

{
  id: 1,
  name: "Some User Name",
  active: true,
  extended: {
      activatedAt: 1640995200000
  }
};
Войти в полноэкранный режим Выйти из полноэкранного режима

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

const saveUser = (user: User) => {
  if (user && user.profile && user.profile.activatedAt) {
    const activationDate = user.profile.activatedAt;
    // do something with the information...
  } else {
    // do something else
  }
};
Вход в полноэкранный режим Выход из полноэкранного режима

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

Расширенный

TypeScript не предлагает никаких возможностей проверки JSON во время выполнения, но в экосистеме TypeScript есть библиотеки, которые мы можем использовать для этого конкретного случая.
Мы будем использовать популярную библиотеку io-ts, чтобы обеспечить надежность данных, с которыми мы работаем, на протяжении всего приложения. Наш подход будет заключаться в декодировании любых внешних данных, поступающих в наше приложение.

Библиотека io-ts написана Джулио Канти и предлагает проверку типов во время выполнения. Для получения дополнительной информации о io-ts обратитесь к README. Так называемые кодеки используются для кодирования/декодирования данных. Эти кодеки являются представлениями определенных статических типов во время выполнения и могут быть составлены для построения еще больших валидаторов типов.

Кодеки позволяют нам кодировать и декодировать любые входящие и исходящие данные, а встроенный метод decode возвращает тип Either, который представляет успех (Right) и неудачу (Left). Используя эту функциональность, мы можем декодировать внешние данные и обрабатывать случаи успеха/неудачи. Для лучшего понимания давайте восстановим наш предыдущий пример, используя библиотеку io-ts.

import * as t from "io-ts";

const User = t.type({
  id: t.number,
  name: t.string,
  active: t.boolean,
  profile: t.type({
    activatedAt: t.number,
  }),
});
Вход в полноэкранный режим Выход из полноэкранного режима

Комбинируя различные кодеки, такие как string или number, мы можем построить тип исполнения User, который мы можем использовать для проверки любых входящих данных user.

Предыдущая базовая конструкция имеет ту же форму, что и тип User, который мы определили ранее. Однако мы не хотим переопределять User как статический тип. Здесь нам может помочь io-ts, предложив TypeOf, который позволяет земле пользователя генерировать статическое представление построенного User.

type UserType = t.TypeOf<typeof User>;
Вход в полноэкранный режим Выйти из полноэкранного режима

Интересно, что это даст нам то же представление, которое мы определили в начале:

type UserType = {
  id: number,
  name: string,
  active: boolean,
  profile: {
    activatedAt: number,
  },
};
Вход в полноэкранный режим Выход из полноэкранного режима

После того, как мы определили форму, мы можем проверить, имеют ли данные ожидаемую форму, и обработать успешный или неуспешный случай:

const userA = {
  id: 1,
  name: "Test User A",
  active: true,
  profile: {
    activatedAt: t.number,
  },
};

const result = User.decode(userA);

if (result._tag === "Right") {
  // handle the success case
  // access the data
  result.right;
} else {
  // handle the failure
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Результат функции декодирования содержит свойство _tag, которое может быть либо строкой Right, либо Left, что означает успех или неудачу. Кроме того, мы имеем доступ к свойствам right и left, содержащим декодированные данные в случае успеха (справа) или сообщение об ошибке в случае неудачи (справа).
Приведенный выше пример может быть расширен для использования так называемого PathReporter для обработки сообщений об ошибках:

import { PathReporter } from "io-ts/lib/PathReporter";

if (result._tag === "Right") {
  // handle the success case
  // access the data
  result.right;
} else {
  // handle the failure
  console.warn(PathReporter.report(result).join("n"));
}
Вход в полноэкранный режим Выход из полноэкранного режима

io-ts также поставляется с fp-ts в качестве зависимой части, которая предлагает такие полезные функции, как isRight или fold. Мы можем использовать функцию isRight для проверки правильности результата декодирования, вместо того, чтобы вручную обрабатывать его через свойство _tag.

import * as t from "io-ts";
import { isRight } from "fp-ts/lib/Either";

const userA = {
  id: 1,
  name: "Test User A",
  active: true,
  profile: {
    activatedAt: t.number,
  },
};

isRight(User.decode(userA)); // true

const userB = {
  id: 1,
  name: "Test User",
  active: true,
  extended: {
    activatedAt: t.number,
  },
};

isRight(User.decode(userB)); // false
Вход в полноэкранный режим Выход из полноэкранного режима

Еще одна полезная функция, которая поможет нам при работе с типом Either, который возвращает декодирование, это fold, которая позволяет нам определить путь успеха и неудачи, смотрите следующий пример для более подробного объяснения:

const validate = fold(
  (error) => console.log({ error }),
  (result) => console.log({ result })
);

// success case
validate(User.decode(userA));

// failure case
validate(User.decode(userB));
Войти в полноэкранный режим Выход из полноэкранного режима

Использование fold позволяет нам обрабатывать корректные или некорректные данные при вызове нашей функциональности fetch. Функция loadUser теперь может быть рефакторингована для обработки этих случаев.

const resolveUser = fold(
  (errors: t.Errors) => {
    throw new Error(`${errors.length} errors found!`);
  },
  (user: User) => saveUser(user)
);

const loadUser = (id: number) => {
  fetch(`http://www.your-defined-endpoint.com/users/${id}`)
    .then((response) => response.json())
    .then((user) => resolveUser(User.decode(user)))
    .catch((error) => {
      console.log({ error });
    });
};
Вход в полноэкранный режим Выход из полноэкранного режима

Мы можем обработать любое неправильное представление, выбросив еще одну ошибку. Это предотвращает передачу данных в нашем приложении. Здесь можно сделать еще больше улучшений. Сейчас мы очень специфичны в том, как мы обрабатываем декодирование User. Возможно, есть возможность написать общую функцию, которая обрабатывает любые данные, основанные на обещаниях.

const decodePromise = <I, O>(type: t.Decoder<I, O>, value: I): Promise<O> => {
  return (
    fold < t.Errors,
    O,
    Promise <
      O >>
        ((errors) => Promise.reject(errors),
        (result) => Promise.resolve(result))(type.decode(value))
  );
};
Вход в полноэкранный режим Выход из полноэкранного режима

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

const loadUser = (id: number) => {
  fetch(`http://www.your-defined-endpoint.com/users/${id}`)
    .then((response) => response.json())
    .then((user) => decodePromise(User, user))
    .then((user: User) => state.saveUser(user))
    .catch((error) => {
      console.log({ error });
    });
};
Вход в полноэкранный режим Выход из полноэкранного режима

Можно было бы сделать еще больше улучшений, но мы должны иметь базовое понимание того, почему может быть полезно проверять любые внешние данные во время выполнения. io-ts предлагает больше возможностей для работы с рекурсивными и опциональными типами. Кроме того, существуют библиотеки типа io-ts-promise, которые предоставляют больше возможностей и полезных помощников, например, вышеупомянутый decodePromise доступен в более продвинутом варианте через io-ts-promise.


Ссылки

io-ts

io-ts-promise


Если у вас есть вопросы или отзывы, пожалуйста, оставьте комментарий здесь или свяжитесь с нами через Twitter: А. Шариф

Оцените статью
Procodings.ru
Добавить комментарий