示例说明
环境变量天然扁平且全是字符串,业务需要的却是带类型带层级的配置对象。Zod 的 transform 把解析与校验合二为一:逗号分隔的特性开关在转换时即校验白名单,未知开关当场报错而不是默默失效;safeParse 聚合全部问题一次性输出,运维改一次配置就能看到所有缺漏。
技术要点
- z.coerce 处理字符串到数字的转型
- transform 内嵌白名单校验逻辑
- 启动时聚合输出全部配置问题
代码解读
loadConfig 把 process.env 整体交给 schema:必填的数据库地址缺失、端口越界、特性开关拼错都会成为 issue;报错消息把路径与原因拼接成清单。导出的 config 带完整类型,isProd 派生常量供全项目引用。
使用建议
敏感配置不要进默认值。特性开关数量增长后改用专用开关服务。staging 与 production 差异项显式列出,防止隐式继承导致的环境漂移。
代码示例
import { z } from 'zod';
const ConfigSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
DATABASE_URL: z.string().url(),
DATABASE_POOL_MAX: z.coerce.number().int().min(1).max(100).default(10),
REDIS_URL: z.string().url().optional(),
FEATURES: z.string().transform((raw, ctx) => {
const flags = raw.split(',').map((f) => f.trim()).filter(Boolean);
const known = ['audit', 'beta-ui', 'webhooks'] as const;
for (const f of flags) {
if (!known.includes(f as never)) {
ctx.addIssue({ code: 'custom', message: 'unknown feature flag: ' + f });
return z.NEVER;
}
}
return flags;
}).default(''),
});
type AppConfig = z.infer<typeof ConfigSchema>;
function loadConfig(source: Record<string, string | undefined>): AppConfig {
const parsed = ConfigSchema.safeParse(source);
if (!parsed.success) {
const report = parsed.error.issues
.map((i) => i.path.join('.') + ': ' + i.message)
.join('; ');
throw new Error('invalid environment configuration -> ' + report);
}
return parsed.data;
}
export const config = loadConfig(process.env as Record<string, string | undefined>);
export const isProd = config.NODE_ENV === 'production';