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- ')}`);
  }
}