Challenge 3: Separate Public Config From Secrets — Possible Solution ==================================================================== import { z } from "zod"; const PublicConfigSchema = z.object({ NODE_ENV: z.enum(["development", "production", "test"]), PORT: z.coerce.number().int().positive(), API_URL: z.string().url(), }); const SecretsSchema = z.object({ DATABASE_PASSWORD: z.string().min(1), JWT_SIGNING_KEY: z.string().min(32), }); type PublicConfig = z.infer; type Secrets = z.infer; function loadPublicConfig(env: NodeJS.ProcessEnv = process.env): PublicConfig { return PublicConfigSchema.parse(env); } function loadSecrets(env: NodeJS.ProcessEnv = process.env): Secrets { return SecretsSchema.parse(env); } export const config = loadPublicConfig(); export const secrets = loadSecrets(); // This function's parameter type is PublicConfig — Secrets simply cannot // be passed to it without a TypeScript error, so "logging a secret by // accident" becomes a compile-time impossibility, not just a code-review hope. function logStartupConfig(publicConfig: PublicConfig): void { console.log("Starting with config:", publicConfig); } logStartupConfig(config); // ✅ compiles // logStartupConfig(secrets); // ❌ Argument of type 'Secrets' is not // assignable to parameter of type 'PublicConfig' WHY THIS WORKS -------------- - PublicConfig and Secrets are structurally distinct types with no shared fields, so TypeScript won't let a Secrets value flow into a function that expects PublicConfig, even though both ultimately come from process.env. - Any function meant to log, display, or send config over the wire should be typed to accept PublicConfig specifically — making "this can't contain a secret" a property the compiler checks, not just a convention developers have to remember. - Splitting the schemas also means secrets can eventually be sourced differently (a secrets manager) without touching how PublicConfig is loaded or validated.