Прощай, process.env.UNDEFINED: типобезопасные переменные окружения в проекте
1. «Ловушка» process.env
В приложениях Node.js доступ к переменным окружения осуществляется через process.env. Это простой и эффективный механизм, однако у него есть врождённая проблема: отсутствие типобезопасности.
Значение в объекте process.env имеет тип string либо undefined. Из-за этого возникает несколько распространённых проблем:
- Неожиданный
undefined: если забыть задать переменную в файле.envили на сервере, во время выполненияprocess.env.MY_VARокажется равнымundefined. Это может привести кTypeErrorглубоко внутри логики приложения. - Несоответствие типов: можно ожидать, что номер порта имеет тип
number, ноprocess.env.PORTвсегда является строкой, поэтому в каждом месте использования приходится вручную вызыватьparseInt. - Разрозненная логика проверки: мы часто пишем защитный код в разных частях проекта, например
const port = process.env.PORT || 3000;, из-за чего управление переменными окружения становится хаотичным.
2. Основной принцип: немедленная ошибка (Fail-Fast)
Для такой критически важной конфигурации, как переменные окружения, рекомендуется применять принцип немедленной ошибки (Fail-Fast).
Это значит, что в самом начале запуска приложение должно проверить наличие и правильность формата всех обязательных переменных окружения. При любой проблеме оно должно сразу выдать ошибку и остановиться, а не аварийно завершиться позднее, в непредсказуемый момент, из-за неверной конфигурации.
Так проблемы конфигурации обнаруживаются сразу при развёртывании или разработке, а не в момент, когда к приложению обращается пользователь.
3. Типобезопасная проверка с помощью Zod
Zod — библиотека для объявления и проверки схем, изначально спроектированная для TypeScript. Она прекрасно подходит для решения проблемы типобезопасности переменных окружения.
Наша стратегия:
- Определить схему Zod для всех переменных окружения.
- При запуске приложения разобрать
process.envс помощью этой схемы. - Если разбор не удался, то есть данные не прошли проверку, Zod выдаёт ошибку, а запуск приложения завершается неудачей.
- Если разбор успешен, мы получаем полностью типизированный объект и используем его во всём приложении.
Пример реализации
Сначала установим Zod: pnpm add zod
Затем создадим отдельный файл для переменных окружения, например src/env.ts:
// src/env.ts
import { z } from 'zod';
// 1. 定义 Schema
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required.'),
PORT: z.coerce.number().int().positive().default(3000),
// z.coerce 会尝试将字符串转换为数字
});
// 2. 解析和导出
// .parse 会在验证失败时抛出错误,实现 "Fail-Fast"
export const env = envSchema.parse(process.env);Теперь из src/env можно импортировать объект env в любое другое место приложения.
// src/server.ts
import { env } from './env'; // 导入经过验证和类型化的 env 对象
// env.PORT 的类型是 `number`,而不是 `string | undefined`
const port = env.PORT;
// env.NODE_ENV 的类型是 'development' | 'production' | 'test'
if (env.NODE_ENV === 'development') {
console.log('Running in development mode');
}
// 如果 DATABASE_URL 未设置,应用在启动时就已经崩溃了,
// 所以在这里我们可以放心地认为它是存在的,并且类型是 `string`。
connectToDatabase(env.DATABASE_URL);4. Преимущества
Этот подход сразу приносит заметную пользу:
- Полная типобезопасность: обращаясь в коде к
env.PORT, TypeScript знает, что этоnumber. - Централизованные документация и проверка: сам файл
env.tsстановится авторитетным описанием переменных окружения, и вся логика проверки находится в одном месте. - Надёжное выполнение: исключаются ошибки времени выполнения из-за опечаток, отсутствующих переменных окружения или неверного формата.
- Fail-Fast: любая проблема конфигурации обнаруживается в самом начале развёртывания или разработки.
Заключение
Перенос проверки переменных окружения на момент запуска приложения и обеспечение типобезопасности с помощью такого инструмента, как Zod, — инженерная практика с очень высокой отдачей. Она заметно повышает надёжность приложения и уверенность разработчиков и является необходимой частью современного проекта на TypeScript.