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-codegenFree 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 | undefinedBefore 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 numberPredictable 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.
- 1typed overridespassed to loadConfig()
- 2sourcean explicit record, or process.env when omitted
- 3dotenv fileslater entries beat earlier ones
- 4compiled defaultsJSON or YAML, validated and embedded at generation
- 5inline defaultsJSDoc, decorator, or class initializer
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
Source on GitHub
MIT licensed. Two packages, one workspace, no hidden service.
CI, every commit
Workspace checks on Bun 1.4.1, plus packed-package consumer tests on Node.js 22, 24 and 26.
Runnable examples
Interface and class projects, plus Fastify and Hono apps. Real workspaces you can run, not snippets.
Changelog
Both packages are versioned and released together. Pin them during early adoption.
Stop maintaining three copies of your config.
Not sure it fits? The comparison page says plainly where Typespun loses.