DevGang
Авторизоваться

Typescript: Валидация внешних данных в полностековых приложениях

В процессе использования langchain.js с typescript для реализации мощных возможностей "вызова функций" я впервые познакомился с фреймворком zod для определения схем функций. Я был очарован осмысленным синтаксисом объявления схемы и решил разобраться в нем, чтобы лучше понять его использование и возможности. В процессе поиска я наткнулся на это видео на YouTube от ByteGrad, и быстро все стало для меня предельно ясно: "ВСЕГДА используйте zod в typescript-приложениях", и ниже я объясню основные причины такого выбора.

Проблема с внешними данными

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

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

  1. Сервера бэкенда: Ваш собственный бэкенд может передавать данные, которые потребляет фронтенд. Однако изменения в структуре данных бэкенда могут повлиять на функциональность фронтенда.
  2. Сторонних API: Данные из сторонних источников могут не всегда соответствовать ожидаемым структурам. В частности, из нестабильных облачных API, которые могут быть изменены без предварительного уведомления.
  3. Ввода данных пользователем: Пользователи могут предоставлять данные через формы, которые могут отличаться по формату и содержанию.
  4. Локального хранилища: Данные, получаемые из локального хранилища, могут со временем измениться или иметь не тот формат, который ожидается.
  5. Параметров URL: Данные также могут храниться и извлекаться из URL, например, параметры поиска.

Примеры использования: Сервер бэкенда / Сторонние API

Для простоты в качестве показательных примеров проверки внешних источников данных, мы приведём бэкенд-сервер и сторонние API.

Валидация схемы при проектировании

Typescript - сильно типизированный язык, позволяющий задавать типы данных. Его статическая проверка типов позволяет гарантировать, что переменные соответствуют назначенным им типам. Такая проверка типов происходит как во время разработки в IDE, так и во время компиляции/транспиляции. Однако когда typescript сталкивается с необходимостью проверки формы внешних данных, статистической проверки типов может оказаться недостаточно. Например, мы можем определить новый тип Product с формой, которую мы ожидаем получить в результате вызова сервера.

// typescript type: design time schema declaration
type Product = {
    name: string;
    price: number;
}

export default function queryProduct() {

  fetch(`/api/product/${productId}`)
    .then((res) => res.json())
    .then((product: Product) => { // assume that the data returned by server is compliant with our schema

      console.log( `product: ${product.name} - ${product.price}` );
    });
}

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

К сожалению, если это не так, мы рискуем тем, что наш код может сломаться во время использования данных, если мы не выполним специальную валидацию, разрабатывая скучный и изматывающий код, который (что самое страшное) мы ДОЛЖНЫ поддерживать в синхронизации со схемой данных. Ниже приведен пример функции проверки данных.

// validate product data - runtime validation
function validateProduct(data: any): Product {
    if (typeof data !== 'object' || data === null) {
        return false;
    }

    if (!('name' in data) || typeof data.name !== 'string') {
        return false;
    }

    if (!('price' in data) || typeof data.price !== 'number') {
        return false;
    }

    return true;
}

Очевидно, что для примера я привел простую схему данных, но можно легко представить, что сложность кода будет расти линейно с увеличением сложности схемы данных.

Решение: Валидаторы схем - Zod вступает в игру!

Чтобы убедиться в том, что данные имеют требуемую форму, и избежать проблем, о которых говорилось ранее, разработчики могут использовать валидаторы схем. Такая библиотека, как zod, позволяет проверять форму данных во время выполнения программы, обеспечивая их соответствие ожидаемой схеме. zod проста в использовании и эффективна, поэтому давайте применим ее к рассмотренному нами коду.

// zod object schema: run time schema declaration
const productSchema = z.object({
  name: z.string(),
  price: z.number(),
});

// typescript type: design time schema declaration
type Product = {
    name: string;
    price: number;
}

export default function Product() {
  useEffect(() => {
    fetch("/api/product")
      .then((res) => res.json())
      .then((product: Product) => {

        // use zod to validate the product
        const validatedProduct = productSchema.safeParse(product); // no exceptions thrown

        if (!validatedProduct.success) {
          console.error(validatedProduct.error);
        }

        console.log( validatedProduct.data );
      });
  }, []);
}

Как видно, zod позволяет построить объявление схемы во время выполнения (z.object(...)) и предоставляет методы для ее проверки на соответствие внешним данным, поступающим от третьих лиц.

Синхронизация определений схем с типами в Zod

Это защищает нас от неожиданных изменений внешних данных, делая наш код гораздо более надежным и прочным. Однако у нас все еще остается проблема синхронизации определения схемы с определением типов typescript (как уже говорилось, именно оно помогает нам в разработке приложения), но не волнуйтесь, zod подумал и об этом, добавив ключевое слово infer, которое способно вывести тип typescript из определения схемы объекта. Магия? Вовсе нет, просто команда zod применила многие из бесконечных возможностей, предоставляемых системой типов, реализованной в typescript. Давайте в последний раз прорефакторим код с такой возможностью!

// zod object schema: run time schema declaration
const productSchema = z.object({
  name: z.string(),
  price: z.number(),
});

// typescript type inferred by object schema definition. It is equivalent of
// type Product = {
//     name: string;
//     price: number;
// }
type Product = z.infer<typeof productSchema>;

export default function Product() {
  useEffect(() => {
    fetch("/api/product")
      .then((res) => res.json())
      .then((product: Product) => {

        // use zod to validate the product
        const validatedProduct = productSchema.safeParse(product); // no exceptions thrown

        if (!validatedProduct.success) {
          console.error(validatedProduct.error);
        }

        console.log( validatedProduct.data );
      });
  }, []);
}

Заключение

Хотя typescript является бесценным инструментом для проверки типов, при работе с внешними источниками данных в полностековых приложениях необходимо использовать валидаторы схем. Проверяя форму внешних данных, разработчики могут обеспечить прочность и надежность своих приложений, предотвращая возможные ошибки и уязвимости. Как уже было сказано, я буду использовать такие инструменты, как zod, когда мне это понадобится, и настоятельно рекомендую вам делать то же самое, а пока... удачного кодинга!

Источник:

#TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу