TypeScript: Typed Config from process.env with Zod (Fail Fast, Defaults, --env-file)
November 6, 2026
Welcome to episode 35 of TypeScript from Zero, a series for complete beginners (no JavaScript needed). Welcome back to TypeScript from zero! Programs need settings: a port number, a secret key, a level of logging. They come from environment variables, and every environment variable is just a text, or missing. Today we turn them into a typed config object. A new file called config.ts. Every code block below is the exact code and output from the video, ready to copy.
- All episodes are in the TypeScript from Zero playlist.
- Versions used: Node.js 26.13.0, npm 11.20.0, TypeScript 7.0.2.
Setup
We start in the terminal and open the project in VS Code:
cd tasks
touch src/config.ts
code .1. Schema
Process.env is the object with all variables. We describe the ones that we need with Zod. The port arrives as text, so z dot coerce dot number turns it into a number, and then we check that it is a whole number between one and sixty five thousand. If it is missing, the default is three thousand. The log level must be one of three words, with info as the default. The API key has no default: it is required, and must not be empty.
2. Fail fast
Then we parse process.env. If something is wrong, we print a clear message and stop the program at once with exit code one. That is called failing fast: better to stop at the start than to crash in the middle of the night. If all is well, the config is exported, and its type comes from the schema.
3. Run
A second file prints the config. Without the key, the program stops, and tells us exactly what is missing. With the key in front of the command, it works, and look: the port is a number, three thousand, and the log level is the default.
src/config.ts:
import { z } from "zod";
const ConfigSchema = z.object({
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
LOG_LEVEL: z.enum(["debug", "info", "error"]).default("info"),
API_KEY: z.string().min(1),
});
const result = ConfigSchema.safeParse(process.env);
if (!result.success) {
console.error("Bad configuration:");
console.error(z.prettifyError(result.error));
process.exit(1);
}
export const config = result.data;src/show-config.ts:
import { config } from "./config.ts";
console.log(config);$ node src/show-config.ts
Bad configuration:
โ Invalid input: expected string, received undefined
โ at API_KEY$ API_KEY=secret node src/show-config.ts
{ PORT: 3000, LOG_LEVEL: 'info', API_KEY: 'secret' }4. Bad value
A bad port, the word abc, is stopped as well, with the reason: expected number, received NaN.
$ PORT=abc API_KEY=x node src/show-config.ts
Bad configuration:
โ Invalid input: expected number, received NaN
โ at PORT5. Error
And the types are real. The port is a number, so asking for to upper case is an error. tsc says: property to upper case does not exist on type number. TS2339.
src/show-config.ts:
import { config } from "./config.ts";
console.log(config);
console.log(config.PORT.toUpperCase());$ npx tsc --noEmit
src/show-config.ts:3:25 - error TS2339: Property 'toUpperCase' does not exist on type 'number'.
3 console.log(config.PORT.toUpperCase());
~~~~~~~~~~~
Found 1 error in src/show-config.ts:36. Fix
We delete the line, and tsc is quiet again.
src/show-config.ts:
import { config } from "./config.ts";
console.log(config);$ npx tsc --noEmit7. env file
Typing variables before every command gets old. Node can read a file of variables with the flag -- env dash file. We write the key into a file called.env, and run again.
$ echo "API_KEY=secret" > .env$ node --env-file=.env src/show-config.ts
{ PORT: 3000, LOG_LEVEL: 'info', API_KEY: 'secret' }Recap
Today: environment variables as text, a Zod schema with coerce, defaults and enum, failing fast, a typed config object, and the env file flag. Next episode: an HTTP server.