Typed Environment Validation At Startup
A startup validator that converts raw environment strings into typed values once and fails with a complete list of problems.
Code
export interface EnvSpec<T> {
readonly name: string;
readonly parse: (raw: string) => T;
readonly required?: boolean;
readonly defaultValue?: T;
readonly min?: number;
readonly max?: number;
}
export class EnvValidationError extends Error {
readonly problems: string[];
constructor(problems: string[]) {
super("invalid environment:\n " + problems.join("\n "));
this.name = "EnvValidationError";
this.problems = problems;
}
}
function boundsProblem<T>(spec: EnvSpec<T>, value: T): string | null {
if (typeof value !== "number") return null;
const n = value as number;
if (spec.min !== undefined && n < spec.min) return spec.name + " below minimum " + spec.min;
if (spec.max !== undefined && n > spec.max) return spec.name + " above maximum " + spec.max;
return null;
}
/**
* Parse declared environment variables once, aggregating every problem.
* A default applies only when the variable is absent or empty and not required.
*/
export function loadEnv<T extends Record<string, unknown>>(
specs: readonly EnvSpec<unknown>[],
env: Record<string, string | undefined> = process.env,
): T {
const config: Record<string, unknown> = {};
const problems: string[] = [];
for (const spec of specs) {
const raw = env[spec.name];
if (raw === undefined || raw === "") {
if (spec.defaultValue !== undefined) {
config[spec.name] = spec.defaultValue;
continue;
}
if (spec.required !== false) {
problems.push(spec.name + " is required but missing");
}
continue;
}
let parsed: unknown;
try {
parsed = spec.parse(raw);
} catch {
problems.push(spec.name + " could not be parsed from " + JSON.stringify(raw));
continue;
}
// parseInt 等 parser 对无效输入返回 NaN 而不抛错,必须显式判定为解析失败
if (typeof parsed === "number" && Number.isNaN(parsed)) {
problems.push(spec.name + " could not be parsed from " + JSON.stringify(raw));
continue;
}
const problem = boundsProblem(spec, parsed);
if (problem) {
problems.push(problem);
continue;
}
config[spec.name] = parsed;
}
if (problems.length > 0) throw new EnvValidationError(problems);
return config as T;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.