Интеграция с TypeScript

Интеграция с TypeScript строится вокруг идеи синхронизации JSON Schema и статических типов. Основная проблема заключается в том, что JSON Schema описывает структуру данных во время выполнения, тогда как TypeScript работает на этапе компиляции. При неправильной организации возникает расхождение между схемой валидации и типами, что приводит к ошибкам, обнаруживаемым только в рантайме.

В экосистеме Ajv ключевым механизмом типизации становится использование генерации типов из схем и строгая проверка соответствия схем типам через JSONSchemaType.


Использование JSONSchemaType как базового источника правды

TypeScript-ориентированный подход в Ajv предполагает, что схема становится первичной моделью данных.

import Ajv, { JSONSchemaType } from "ajv";

const ajv = new Ajv();

interface User {
  id: number;
  name: string;
  email?: string;
}

const schema: JSONSchemaType<User> = {
  type: "object",
  properties: {
    id: { type: "integer" },
    name: { type: "string" },
    email: { type: "string", nullable: true },
  },
  required: ["id", "name"],
  additionalProperties: false,
};

const validate = ajv.compile(schema);

Здесь JSONSchemaType<User> выполняет роль строгого контракта. Любое несоответствие между интерфейсом и схемой приводит к ошибке компиляции.


Строгая синхронизация типов и схем

Главная особенность интеграции заключается в том, что TypeScript не проверяет JSON Schema сам по себе. Проверка происходит через дженерики и строгую типизацию полей схемы.

Ошибки, которые предотвращаются:

  • несоответствие type и TypeScript-типа
  • отсутствие обязательных полей
  • неверная структура вложенных объектов
  • использование неподдерживаемых комбинаций nullable, required, enum

Пример ошибки:

const schema: JSONSchemaType<User> = {
  type: "object",
  properties: {
    id: { type: "string" }, // ошибка: должен быть integer
    name: { type: "string" },
  },
  required: ["id", "name"],
  additionalProperties: false,
};

TypeScript остановит компиляцию, так как id не соответствует типу number.


Инференс типов из схем

В современных подходах Ajv допускает обратную стратегию — получение TypeScript-типа из схемы.

Это особенно полезно, когда схема является источником правды в API или при работе с внешними контрактами.

import { FromSchema } from "json-schema-to-ts";

const userSchema = {
  type: "object",
  properties: {
    id: { type: "integer" },
    name: { type: "string" },
  },
  required: ["id", "name"],
  additionalProperties: false,
} as const;

type User = FromSchema<typeof userSchema>;

Такой подход снижает дублирование и исключает рассинхронизацию между моделью данных и схемой.


Компиляция схем и типизация валидаторов

Ajv предоставляет механизм компиляции схемы в функцию-валидатор. В TypeScript важно сохранять типизацию результата compile.

const validate = ajv.compile<User>(schema);

const data: User = {
  id: 1,
  name: "Alex",
};

if (validate(data)) {
  // data имеет тип User
} else {
  console.log(validate.errors);
}

Здесь происходит связка:

  • схема определяет структуру
  • TypeScript гарантирует тип входных данных
  • валидатор обеспечивает runtime-проверку

Работа с nullable и optional полями

TypeScript и JSON Schema имеют различную семантику для отсутствующих и null значений.

В Ajv важно явно различать:

  • required — обязательное наличие ключа
  • nullable: true — допустимость null
  • отсутствие в required — опциональность
interface Profile {
  bio?: string;
  age: number | null;
}

const schema: JSONSchemaType<Profile> = {
  type: "object",
  properties: {
    bio: { type: "string", nullable: true },
    age: { type: "integer", nullable: true },
  },
  required: ["age"],
  additionalProperties: false,
};

Ошибки чаще всего возникают при попытке совместить undefined и null без явного отражения в схеме.


Generic-валидация и повторное использование схем

Типизация в Ajv позволяет строить универсальные фабрики схем.

function createValidator<T>(schema: JSONSchemaType<T>) {
  return ajv.compile<T>(schema);
}

Это обеспечивает:

  • повторное использование логики
  • строгую типизацию на уровне фабрики
  • минимизацию дублирования кода

Расширенные типы: union, enum, const

TypeScript и JSON Schema расходятся в описании объединений типов, но Ajv корректно поддерживает их через oneOf, enum и const.

Enum

interface Role {
  role: "admin" | "user";
}

const schema: JSONSchemaType<Role> = {
  type: "object",
  properties: {
    role: { type: "string", enum: ["admin", "user"] },
  },
  required: ["role"],
  additionalProperties: false,
};

Union через oneOf

type Shape =
  | { type: "circle"; radius: number }
  | { type: "square"; size: number };
const schema: JSONSchemaType<Shape> = {
  oneOf: [
    {
      type: "object",
      properties: {
        type: { const: "circle" },
        radius: { type: "number" },
      },
      required: ["type", "radius"],
    },
    {
      type: "object",
      properties: {
        type: { const: "square" },
        size: { type: "number" },
      },
      required: ["type", "size"],
    },
  ],
};

Изоляция runtime и compile-time логики

Ключевой архитектурный принцип при работе с Ajv и TypeScript заключается в разделении ответственности:

  • TypeScript гарантирует корректность кода до запуска
  • Ajv гарантирует корректность данных во время выполнения

Такое разделение позволяет:

  • безопасно принимать данные из внешних источников
  • избегать дублирования проверок
  • поддерживать строгие API-контракты

Проблемы совместимости типов и схем

Типовые расхождения:

  • number vs integer
  • undefined vs отсутствие поля
  • readonly свойства TypeScript не отражаются в JSON Schema
  • различия в обработке null

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


Практика строгих контрактов в API

В связке с TypeScript Ajv часто используется как слой валидации входящих данных API:

  • DTO описывается через интерфейс
  • схема строго повторяет DTO
  • валидатор используется в middleware
app.post("/user", (req, res) => {
  if (!validate(req.body)) {
    return res.status(400).json(validate.errors);
  }

  const user: User = req.body;
});

Такой подход минимизирует риск некорректных данных на уровне бизнес-логики.


Типизация пользовательских ключей и словарей

Сложные структуры, такие как record-объекты, требуют аккуратного описания:

interface Dictionary {
  [key: string]: number;
}
const schema: JSONSchemaType<Dictionary> = {
  type: "object",
  additionalProperties: { type: "number" },
};

Ajv требует явного указания additionalProperties, иначе структура считается неопределённой.


Поддержка строгих пайплайнов данных

В зрелых проектах интеграция TypeScript и Ajv используется как часть цепочки обработки данных:

  1. получение внешнего JSON
  2. runtime-валидация через compiled schema
  3. приведение к TypeScript-типу
  4. использование в бизнес-логике

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