EnvConfig reader with typed getters
TypeScript
export type Env = Record<string, string | undefined>;
const processEnv = (globalThis as { process?: { env?: Env } }).process?.env ?? {};
/** Read typed settings from the environment and collect validation errors. */
export class EnvConfig {
private readonly errors: string[] = [];
constructor(private readonly env: Env = processEnv) {}
string(name: string, options: { required?: boolean; fallback?: string } = {}): string {
const raw = this.env[name]?.trim();
if (raw) return raw;
if (options.required) this.errors.push(`${name} is required`);
return options.fallback ?? '';
}
integer(name: string, options: { fallback: number; min?: number; max?: number }): number {
const raw = this.env[name]?.trim();
if (!raw) return options.fallback;
const value = Number(raw);
if (!Number.isInteger(value)) this.errors.push(`${name} must be an integer, got '${raw}'`);
else if (options.min !== undefined && value < options.min) this.errors.push(`${name} must be >= ${options.min}`);
else if (options.max !== undefined && value > options.max) this.errors.push(`${name} must be <= ${options.max}`);
return value;
}
boolean(name: string, fallback = false): boolean {
const raw = this.env[name]?.trim().toLowerCase();
if (!raw) return fallback;
if (['1', 'true', 'yes', 'on'].includes(raw)) return true;
if (['0', 'false', 'no', 'off'].includes(raw)) return false;
this.errors.push(`${name} must be a boolean, got '${raw}'`);
return fallback;
}
/** Throw once with every invalid variable listed. */
assertValid(): void {
if (this.errors.length > 0) throw new Error(`Invalid environment:\n- ${this.errors.join('\n- ')}`);
}
}