Конфигурации приложений в JavaScript часто представляют собой набор объектов, поступающих из внешних источников: переменных окружения, JSON-файлов, API или пользовательского ввода. Основная проблема таких данных — отсутствие гарантированной структуры и типов на этапе выполнения.
Типобезопасные конфигурации решают эту проблему через явное описание схемы данных и автоматическую проверку соответствия входных значений. В Zod схема становится одновременно:
Ключевой принцип заключается в том, что конфигурация описывается один раз, а затем используется и для проверки, и для вывода типов.
Структура конфигурации обычно описывается через
z.object, где каждая ключ-значение пара получает строгий
тип.
import { z } from "zod";
const configSchema = z.object({
port: z.number(),
host: z.string(),
debug: z.boolean()
});
На основании этой схемы автоматически выводится тип:
type Config = z.infer<typeof configSchema>;
Результирующий тип:
type Config = {
port: number;
host: string;
debug: boolean;
};
Таким образом устраняется дублирование: нет необходимости отдельно описывать интерфейс и валидатор.
Основной метод работы с конфигурацией — parse.
const config = configSchema.parse({
port: 3000,
host: "localhost",
debug: true
});
Если данные не соответствуют схеме, выполнение прерывается с ошибкой. Это критично для конфигураций, поскольку некорректные значения на старте приложения приводят к непредсказуемым сбоям.
Альтернативный вариант — safeParse, позволяющий избежать
исключений:
const result = configSchema.safeParse(input);
if (!result.success) {
console.error(result.error);
} else {
const config = result.data;
}
Одна из наиболее распространённых задач — построение конфигурации на
основе process.env.
Переменные окружения всегда приходят в виде строк, поэтому требуется преобразование типов:
const envSchema = z.object({
PORT: z.string().transform((val) => Number(val)),
HOST: z.string(),
DEBUG: z.string().transform((val) => val === "true")
});
Более строгий вариант использует предварительную обработку:
const envSchema = z.object({
PORT: z.coerce.number(),
HOST: z.string(),
DEBUG: z.coerce.boolean()
});
z.coerce автоматически преобразует строковые значения в
нужный тип, сохраняя строгую типизацию результата.
Конфигурации часто требуют дефолтных значений. В Zod это реализуется
через default:
const configSchema = z.object({
port: z.number().default(3000),
host: z.string().default("localhost"),
debug: z.boolean().default(false)
});
При отсутствии поля в входных данных используется значение по умолчанию, но тип остаётся строго определённым.
Важно, что default влияет на runtime-значение, но не
делает поле необязательным в исходной схеме.
Для гибких конфигураций используются optional и
partial.
const configSchema = z.object({
port: z.number(),
host: z.string().optional()
});
В этом случае host может отсутствовать, но тип
становится:
host?: string | undefined;
Метод partial применяется для преобразования всей схемы
в частично необязательную:
const partialConfig = configSchema.partial();
Это полезно для многоуровневых конфигураций, где часть значений может дополняться на разных этапах.
При работе с вложенными объектами требуется глубокая типизация.
const configSchema = z.object({
server: z.object({
port: z.number(),
host: z.string()
}),
database: z.object({
url: z.string(),
poolSize: z.number()
})
});
Для частичной модификации вложенных структур используется
deepPartial:
const partialConfig = configSchema.deepPartial();
Это превращает все вложенные поля в необязательные, сохраняя структуру объекта.
Типобезопасные конфигурации часто строятся по модульному принципу.
const baseConfig = z.object({
port: z.number(),
host: z.string()
});
const loggingConfig = z.object({
logLevel: z.string()
});
const appConfig = baseConfig.merge(loggingConfig);
Результирующий тип объединяет оба набора полей.
Также используется extend:
const extended = baseConfig.extend({
debug: z.boolean()
});
Разница заключается в том, что extend применяется только
к объектам, а merge может объединять более сложные
схемы.
Конфигурации часто требуют приведения к более удобной форме.
const configSchema = z.object({
port: z.string()
}).transform((data) => ({
port: Number(data.port)
}));
После трансформации тип автоматически меняется:
type Config = {
port: number;
};
Это позволяет объединять валидацию и нормализацию данных в одном месте.
Для сложных правил используется refine:
const configSchema = z.object({
port: z.number()
}).refine((data) => data.port > 0 && data.port < 65536, {
message: "Недопустимый диапазон порта"
});
refine не изменяет тип, но накладывает дополнительное
логическое ограничение.
Для типобезопасных конфигураций это особенно важно при проверке бизнес-ограничений.
В системах с несколькими режимами работы применяется discriminated union:
const configSchema = z.discriminatedUnion("mode", [
z.object({
mode: z.literal("development"),
debug: z.boolean()
}),
z.object({
mode: z.literal("production"),
minify: z.boolean()
})
]);
Это позволяет строго разделить конфигурации по режимам среды выполнения.
Типизация становится зависимой от значения поля
mode.
Для контроля структуры конфигурации используется строгий режим:
const configSchema = z.object({
port: z.number()
}).strict();
Любые дополнительные поля вызывают ошибку валидации.
Альтернативный вариант — strip, который удаляет лишние
поля без ошибки.
Типобезопасные конфигурации часто выстраиваются как отдельный слой приложения:
const config = configSchema.parse(process.env);
После этого config становится единственным источником
правды для всей системы.
При росте приложения конфигурации разделяются на модули:
const httpConfig = z.object({
port: z.number(),
host: z.string()
});
const dbConfig = z.object({
url: z.string(),
pool: z.number()
});
const configSchema = z.object({
http: httpConfig,
db: dbConfig
});
Такой подход позволяет:
Одним из ключевых свойств Zod является синхронизация runtime-валидации и TypeScript-типа.
Любое изменение схемы автоматически отражается в типах, что приводит к:
Типобезопасные конфигурации становятся не вспомогательным механизмом, а частью архитектурного каркаса приложения.