Прощай, process.env.UNDEFINED: типобезопасные переменные окружения в проекте

3 мин

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. Она прекрасно подходит для решения проблемы типобезопасности переменных окружения.

Наша стратегия:

  1. Определить схему Zod для всех переменных окружения.
  2. При запуске приложения разобрать process.env с помощью этой схемы.
  3. Если разбор не удался, то есть данные не прошли проверку, Zod выдаёт ошибку, а запуск приложения завершается неудачей.
  4. Если разбор успешен, мы получаем полностью типизированный объект и используем его во всём приложении.

Пример реализации

Сначала установим 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.