Работа со схемами валидации часто приводит к необходимости синхронизации двух миров: runtime-валидации и статической типизации. В экосистеме JavaScript библиотека Joi традиционно решает задачу проверки данных в рантайме, тогда как TypeScript отвечает за описание структур на этапе компиляции. Разрыв между этими слоями становится источником рассинхронизации типов и схем.
Извлечение типов из схем Joi представляет собой процесс построения TypeScript-типов на основе описаний валидационных правил. В отличие от некоторых альтернативных решений, Joi не предоставляет полноценного встроенного механизма автоматической генерации типов, поэтому применяются комбинированные подходы: статический анализ, ручное отображение и использование метаданных схем.
Схемы Joi описываются декларативно, но их структура ориентирована на runtime-проверки:
const schema = Joi.object({
id: Joi.number().integer().required(),
name: Joi.string().min(3),
tags: Joi.array().items(Joi.string())
});
На уровне TypeScript такая схема не имеет прямого отражения в типах. Основная проблема заключается в том, что:
Это приводит к необходимости внешней трансляции схемы в тип.
Наиболее прямолинейный способ заключается в ручном дублировании структуры:
type User = {
id: number;
name?: string;
tags?: string[];
};
Такой подход прост, но приводит к дублированию логики. Любое изменение схемы требует синхронного обновления типов.
Joi предоставляет метод describe(), позволяющий получить
структурное описание схемы:
const description = schema.describe();
Результат содержит дерево с типами узлов:
{
"type": "object",
"keys": {
"id": { "type": "number" },
"name": { "type": "string", "flags": { "presence": "optional" } }
}
}
Это описание может использоваться как промежуточное представление для генерации типов.
Базовый принцип заключается в рекурсивном обходе структуры
describe() и отображении типов:
string → stringnumber → numberboolean → booleanany → anytype FromJoiObject<T> = {
[K in keyof T]: FromJoiType<T[K]>
}
type FromJoiType<T> =
T extends { type: "string" } ? string :
T extends { type: "number" } ? number :
T extends { type: "boolean" } ? boolean :
T extends { type: "array"; items: any } ? FromJoiType<T["items"]>[] :
T extends { type: "object"; keys: infer K } ? {
[P in keyof K]: FromJoiType<K[P]>
} :
unknown;
Joi использует флаги presence:
requiredoptionalforbiddenПри преобразовании учитывается presence:
type OptionalIf<T, Cond> = Cond extends true ? T | undefined : T;
Пример:
type FieldFromJoi<T> =
T["flags"]["presence"] extends "optional"
? FromJoiType<T> | undefined
: FromJoiType<T>;
Сложность возрастает при вложенных схемах:
Joi.array().items(
Joi.object({
id: Joi.number(),
value: Joi.string()
})
);
Типизация:
type Item = {
id: number;
value: string;
};
type Result = Item[];
Рекурсивный алгоритм должен учитывать:
Конструкция alternatives() в Joi соответствует
union-типам TypeScript:
Joi.alternatives().try(
Joi.string(),
Joi.number()
);
Отображение:
type Result = string | number;
При генерации типов требуется агрегировать все ветви
try().
Для уменьшения ручной работы применяются вспомогательные утилиты:
type InferJoi<T> = T extends { describe: () => infer D }
? FromJoiType<D>
: never;
Использование:
type User = InferJoi<typeof userSchema>;
Однако такие конструкции ограничены тем, что describe()
возвращает runtime-значение, а не статический тип.
В ряде архитектур применяется принцип:
Такой подход снижает связность и уменьшает риск рассинхронизации.
Существуют сторонние решения, работающие поверх Joi:
describe()Часто используется подход, при котором схема экспортируется как JSON
через describe() и далее обрабатывается генератором
типов.
Некоторые конструкции Joi плохо поддаются строгой типизации:
any() — теряет информацию о типеcustom() — произвольная логикаwhen() — условная структураВ таких случаях результатом обычно становится unknown
или расширенный union.
При глубокой вложенности схем возникают ограничения компилятора:
Для сложных схем часто применяется упрощение типов или ограничение глубины вывода.
Типовой процесс построения выглядит следующим образом:
describe() для получения AST-подобной
структурыТакой процесс превращает Joi-схему в источник формального описания данных, пригодный для статического анализа, но требует аккуратного контроля за сложными случаями и нестандартными расширениями схем.