Зод делает машинописный текст еще лучше

Вы когда-нибудь пытались получить доступ к свойству из переменной undefined?

О да, многие из нас! И по многим причинам. Сегодня мы углубимся в одну из таких причин.

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

Во всех этих случаях вы получали нетипизированный объект, который в конечном итоге будет обрабатываться как any, unknown или string, который должен быть literal. И для этого Зод может стать вашим решением!

Определение схем и типов

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

import { z } from "zod";

const mySimpleSchema = z.string(); // schema defines what is the final type
const weirdObject: unknown = "I am an untyped object"; // "mocking" an unknown variable
const coolObject = mySimpleSchema.parse(weirdObject); // parsing object results in typed variable

console.log(typeof coolObject); // string
type InferredFromSchema = z.infer<typeof mySimpleSchema>; // will infer to the TS type that results from the schema, pretty cool!

Так что же здесь произошло? У нас был weirdObject, который пришел нетипизированным, как unknown, а затем, после его разбора, мы убедились, что он соответствует нашей структуре схемы, возвращая затем coolObject, который правильно типизирован. .parse() выдаст ошибку, если объект не соответствует схеме, если вам не нравится механизм throw, вы можете взглянуть на safeParse.

В этом суть!

Глубже в лес

Давайте посмотрим на более реалистичный пример здесь. Представьте, что мне нужно получить данные JSON из API, а возврат имеет некоторые ограничения:

  1. возвращает объект
  2. есть ключ, который опционально добавляется
  3. есть строка, которую нужно отформатировать как URL
  4. есть число, которое должно быть больше или равно 0
  5. есть строка, которая всегда является одним из двух значений: literalA или literalB

Вот как мы можем подтвердить наше возвращение с помощью Zod:

import { z } from "zod";

const randBool = (): boolean => {
  return Math.random() > 0.5
}

const mySillyFetch = (url: string): unknown => {
  const r = {
    url,
    aLiteral: randBool() ? 'literalA' : 'literalB',
    someKey: 'someValue',
    anotherKey: {
      inner: 5,
    },
  }
  if (randBool()) {
    // @ts-expect-error intentional!
    r.anotherKey.random = 10
  }
  return r
}

const dataSchema = z.object({
  someKey: z.string(),
  aLiteral: z.enum(['literalA', 'literalB']),
  anotherKey: z.object({
    inner: z.number().nonnegative(),
    random: z.number().optional(),
  }),
  url: z.string().url(),
})

const data = mySillyFetch('http://example.com')

const typedData = dataSchema.parse(data) // Done!

console.log(typedData)

В этом примере mySillyFetch издевается над fetch, который точно не знает, как устроен объект. Итак, давайте посмотрим, как мы решили эти ограничения:

  1. просто использование z.object() решило нашу проблему, так как в их readme docs есть масса простых и сложных типов;
  2. простая цепочка .optional() решила нашу проблему (взгляните на nullables, NaNs);
  3. просто сцепив .url(), мы можем убедиться, что строка правильно отформатирована (взгляните на strings);
  4. простое использование .nonnegative() решило нашу проблему (взгляните на number для проверки числа);
  5. zod имеет .enum(...) (родной для zod), который хорошо работает с литералами;

Это 5 простых примеров, но я могу заверить вас, что Зод поможет вам в любом типе! Карты, наборы, промисы, функции и даже кастомный!

Уточнение, принуждение и преобразование

Мы можем найти множество функций проверки, но что, если мы захотим создать свою собственную? Что ж, Зод вас тоже прикрыл!

Уточнить

Если вы хотите обеспечить более конкретное правило, вы можете refine:

const myString = z.string().refine((val) => val.length <= 255, {
  message: "String can't be more than 255 characters",
});

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

Принуждение

Если мы хотим применить тип, есть два варианта: принудить и преобразовать. В первом случае мы преобразуем примитивы, такие как приведение во время выполнения (например, Boolean('a')), с помощью преобразования мы можем делать все, что захотим!

Давайте посмотрим, как мы можем принуждать примитивы:

const schema = z.coerce.string();
schema.parse("tuna"); // => "tuna"
schema.parse(12); // => "12"
schema.parse(true); // => "true"

И так как есть приведения для строки, у нас также есть приведения для числа, логического значения (это может быть хитрым), bigint и даты!

Трансформировать

Более мощное принуждение — это transform:

const emailToDomain = z
  .string()
  .email()
  .transform((val) => val.split("@")[1]);

Эта схема всегда будет получать домен после разбора!

Хитрый пример

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

// trivial coersion
const aBooleanFromStringSchema = z.coerce.boolean();
console.log(aBooleanFromStringSchema.parse('false')); // true, is that what we really want?

// now evaluating the string values
const aBooleanFromStringSchema = z
  .string()
  .refine((val) => {
    return val.toLowerCase() === 'true' || val.toLowerCase() === 'false';
  })
  .transform((val) => {
    return val.toLowerCase() === 'true';
  });

console.log(aBooleanFromStringSchema.parse('false')); // false! now it's the correct behavior

Хорошо, это имеет смысл. false на самом деле правдиво, о, javascript...

Проверка ваших envs!

В завершение мы рассмотрим код, который может быть в вашем репозитории достаточно скоро: проверка envs.

// create a file env.ts
import { z } from 'zod';

const envSchema = z.object({
  HOST: z.string().default('localhost'),
  PORT: z.coerce.number().default(6060),
  SOME_BOOL: z
    .string()
    .default('false')
    .refine((val) => {
      return val.toLowerCase() === 'true' || val.toLowerCase() === 'false';
    })
    .transform((val) => {
      return val.toLowerCase() === 'true';
    }),
});

const env = envSchema.parse(process.env);

export default env;

Сделанный! Ваши envs проанализированы!

Преимущества перед конкурентами

Zod — это очень удобная библиотека для разработчиков, которая кратко проверяет ваши объекты, будь то их ввод или проверка их значений во время выполнения. Как указано в документации zod, есть и небольшие преимущества:

  • Нулевые зависимости
  • Работает в Node.js (даже в старых!) и во всех современных браузерах.
  • Tiny: 8 КБ уменьшено + заархивировано
  • Неизменяемый: методы (например, .optional()) возвращают новый экземпляр
  • Лаконичный интерфейс
  • Функциональный подход: парсить, а не валидировать
  • Работает и с простым JavaScript! Вам не нужно использовать TypeScript.

Заключение

Только люди, у которых были кошмары, проверяющие типы и значения в typescript/javascript, могут быть так же взволнованы этой библиотекой, как я. Но я знаю, что многие другие люди в сообществе тоже могут разделить это волнение, поскольку у Zod много спонсоров и большое сообщество, использующее их инструменты (см. экосистема и растущее число загрузок).

Я надеюсь, что вы найдете эту информацию полезной и добавите Zod в свою проверку типов! Хорошего дня кодирования!