For TypeScript services on Bun and Node.js

Your config, typed and validated.
From one TypeScript interface.

Stop describing every setting three times: a type, some process.env parsing, and a runtime schema. Write one interface. Typespun generates the loader that reads your env vars and files, checks every field at startup, and fails with one clear error.

bun add typespunbun add --dev typespun-codegen

Free and MIT-licensed. Runtime plus a dev-time generator. Bun 1.4.1+ or Node.js 22+. Changelog

Coming from Zod, t3-env or envalid? See how it compares

1. Write the declaration

// src/config.ts/** @typespun */export interface AppConfig {  server: { host: string; port: number };  origins: string[];  /** @env DATABASE_URL */  databaseUrl?: string;}

2. Run typespun generate, then use it

// What your app getsconst config = loadConfig();config.server.port; // numberconfig.databaseUrl; // string | undefined

Before and after

One declaration instead of three copies

Hand-rolled

// The same field, described three times.export interface AppConfig {  server: { host: string; port: number };  origins: string[];}const config: AppConfig = {  server: {    host: process.env.APP_SERVER_HOST ?? '127.0.0.1',    // Number('oops') is NaN. Nothing notices.    port: Number(process.env.APP_SERVER_PORT ?? 3000),  },  origins: JSON.parse(process.env.APP_ORIGINS ?? '[]'),};// ...plus a runtime schema elsewhere to validate it.// Three copies. They drift.

With Typespun

// src/config.ts — the only place it is described./** @typespun */export interface AppConfig {  server: { host: string; port: number };  origins: string[];  /** @env DATABASE_URL */  databaseUrl?: string;}

typespun generate writes src/generated/typespun.ts. You commit it.

// src/index.ts — typed, validated at startup.import { loadConfig } from './generated/typespun.js';const config = loadConfig({  envFiles: [{ path: '.env', optional: true }],  source: process.env,  overrides: { server: { port: 4000 } },});

When something is wrong

You find out at startup, not in production

Every field is checked before your app starts. A bad value stops the process with the field path and the environment key it came from, and when several fields fail, you see all of them at once.

Pass a bad value

loadConfig({ source: { APP_PORT: 'not-a-number' } });

Get this, before anything runs

Configuration validation failedinvalid_value port (APP_PORT): Expected a finite number

Predictable by design

Overrides beat env vars. Env vars beat .env files. Always.

Most environment libraries validate a single source. Typespun merges five, in the order below, one field at a time. It picks the highest-priority value that exists, then validates it, so a bad value in a lower source can never break a good one above it.

  1. 1typed overridespassed to loadConfig()
  2. 2sourcean explicit record, or process.env when omitted
  3. 3dotenv fileslater entries beat earlier ones
  4. 4compiled defaultsJSON or YAML, validated and embedded at generation
  5. 5inline defaultsJSDoc, decorator, or class initializer
Read how precedence resolves, per leaf

What you get

Small surface, fewer surprises

One declaration

The annotated interface — or a schema-only class — is the schema. There is nothing to keep in sync.

Reviewable output

Generation is byte-stable for the same inputs, so the loader belongs in version control and typespun check fails CI when it goes stale.

Secret-aware diagnostics

An invalid @secret field reports its path and environment key without the received value or type-specific detail. That is redaction inside Typespun's own errors — not encryption, and no protection from your application's logs.

When not to use it

If you need custom transforms, dynamic or runtime-fetched schemas, async secret providers, or a broad validation ecosystem, use a schema library instead. Typespun asks you to accept a build step, committed output and a narrow type model.

Check it yourself

New, so we show our work.

Typespun is pre-1.0 and young, so there are no case studies or logos here. Everything below is something you can read and run yourself.

latest release
v0.1.5
licensed, free
MIT
packages, one workspace
2
tested in CI
Bun + Node 22/24/26

Stop maintaining three copies of your config.

Not sure it fits? The comparison page says plainly where Typespun loses.

Open the playground