Creating Type-Safe Environment Variables in TypeScript
·
2 min read
typescript
zod
configuration
What We’re Building
A typed environment configuration that validates at startup, provides autocomplete, and fails fast with clear error messages if required variables are missing.
Prerequisites
- TypeScript project
- Basic Zod knowledge (or willingness to learn)
The Approach
- Define a schema for environment variables
- Validate at application startup
- Export typed configuration object
- Handle errors gracefully
Step 1: Install Zod
npm install zod
Step 2: Define the Schema
Create src/env.ts:
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
ENABLE_FEATURE_X: z.coerce.boolean().default(false),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
type Env = z.infer<typeof envSchema>;
Key points:
z.coerce.number()converts string “3000” to number 3000z.coerce.boolean()handles “true”/“false” strings.default()provides fallbacks for optional variables
Step 3: Validate and Export
function validateEnv(): Env {
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:');
console.error(parsed.error.flatten().fieldErrors);
process.exit(1);
}
return parsed.data;
}
export const env = validateEnv();
Step 4: Use Throughout Your App
import { env } from './env';
// Full autocomplete and type safety
const server = app.listen(env.PORT, () => {
console.log(`Running in ${env.NODE_ENV} mode on port ${env.PORT}`);
});
if (env.ENABLE_FEATURE_X) {
// TypeScript knows this is boolean, not string
}
Step 5: Add Custom Transformations
const envSchema = z.object({
// Parse comma-separated list
ALLOWED_ORIGINS: z
.string()
.transform((s) => s.split(',').map((s) => s.trim()))
.default(''),
// Parse JSON
FEATURE_FLAGS: z
.string()
.transform((s) => JSON.parse(s))
.pipe(z.record(z.boolean()))
.default('{}'),
});
The Result
- Immediate startup failure with clear errors if config is wrong
- Full TypeScript autocomplete for
env.VARIABLE_NAME - No more
process.env.THING as stringcasts - Coercion handles string→number/boolean automatically
What I’d Do Differently
Add this validation to your build process too. Catch missing variables in CI, not production.
This pattern has saved me from countless “undefined is not a valid port” errors in production.