make-typed-env
v0.0.4
Published
Type-safe environment variables for TypeScript. Schema-agnostic via Standard Schema, with optional key transformation.
Downloads
516
Maintainers
Readme
make-typed-env
Type-safe environment variables for TypeScript. Validate with any schema library, transform keys however you want.
Features
🛡️ Validates all env vars against a schema — fail fast at startup, not with cryptic undefined in production.
🔮 Fully typed return object — autocomplete, type errors, and zero manual annotations.
🔌 Works with any Standard Schema library — Zod, Valibot, ArkType, and more.
🔑 Accepts process.env, import.meta.env, or any Record<string, unknown>.
🔄 Optional key transformation — pass camelKeys or any function to reshape the output with full type inference.
⚡ Optional caching by reference — opt in with cache: true to validate once per args object.
📋 Replaces .env.sample files — your schema is the documentation for required variables.
🪶 Zero runtime dependencies.
Install
npm install make-typed-envUsage
With process.env
import { makeTypedEnv } from "make-typed-env";
import { z } from "zod";
const getEnv = makeTypedEnv(
z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
DATABASE_URL: z.string(),
SESSION_SECRET: z.string().min(1),
}),
);
const env = getEnv(process.env);
// env.NODE_ENV → 'development' | 'production' | 'test'
// env.DATABASE_URL → string
// env.SESSION_SECRET → stringWith import.meta.env (Vite)
Vite exposes variables prefixed with VITE_ to the client via import.meta.env:
const getPublicEnv = makeTypedEnv(
z.object({
VITE_GOOGLE_MAPS_API_KEY: z.string(),
VITE_STRIPE_PUBLIC_KEY: z.string(),
}),
{ transform: camelKeys },
);
const env = getPublicEnv(import.meta.env);
// env.viteGoogleMapsApiKey → string
// env.viteStripePublicKey → stringWith Deno, Bun, Cloudflare Workers...
It works with any Record<string, unknown> — not tied to Node.js:
// Deno
const env = getEnv(Deno.env.toObject());
// Bun
const env = getEnv(Bun.env);Transforming keys
Pass a transform function in the options to reshape the parsed object. Combine with string-ts for type-safe key transformations:
npm install string-tsimport { makeTypedEnv } from "make-typed-env";
import { camelKeys } from "string-ts";
import { z } from "zod";
const getEnv = makeTypedEnv(
z.object({
DATABASE_URL: z.string(),
SESSION_SECRET: z.string().min(1),
STRIPE_API_KEY: z.string().min(1),
}),
{ transform: camelKeys },
);
const env = getEnv(process.env);
// env.databaseUrl → string
// env.sessionSecret → string
// env.stripeApiKey → stringThe return type is CamelKeys<T> — fully inferred, no manual type annotations needed.
Any of the key transformation functions from string-ts work:
import { snakeKeys, kebabKeys, pascalKeys } from "string-ts";
makeTypedEnv(schema, { transform: snakeKeys });
makeTypedEnv(schema, { transform: kebabKeys });
makeTypedEnv(schema, { transform: pascalKeys });Or write your own:
const getEnv = makeTypedEnv(schema, {
transform: (parsed) => ({
db: parsed.DATABASE_URL,
secret: parsed.SESSION_SECRET,
}),
});Schema libraries
make-typed-env works with any library that implements the Standard Schema spec.
Zod
import { z } from "zod";
const getEnv = makeTypedEnv(
z.object({
PORT: z.coerce.number().default(3000),
DEBUG: z.coerce.boolean().default(false),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
}),
);Valibot
import * as v from "valibot";
const getEnv = makeTypedEnv(
v.object({
PORT: v.pipe(v.unknown(), v.transform(Number)),
DATABASE_URL: v.pipe(v.string(), v.minLength(1)),
}),
);ArkType
import { type } from "arktype";
const getEnv = makeTypedEnv(
type({
PORT: "string",
DATABASE_URL: "string",
}),
);Patterns
Splitting public and server environments
Separate variables safe for the client from server-only secrets:
// env.shared.ts
import { makeTypedEnv } from "make-typed-env";
import { camelKeys } from "string-ts";
import { z } from "zod";
const publicSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
VITE_STRIPE_PUBLIC_KEY: z.string().min(1),
VITE_SENTRY_DSN: z.string().optional(),
});
const getPublicEnv = makeTypedEnv(publicSchema, { transform: camelKeys });
export { getPublicEnv, publicSchema };// env.server.ts
import { makeTypedEnv } from "make-typed-env";
import { camelKeys } from "string-ts";
import { z } from "zod";
import { publicSchema } from "./env.shared";
const serverSchema = publicSchema.extend({
DATABASE_URL: z.string().min(1),
SESSION_SECRET: z.string().min(1),
STRIPE_SECRET_KEY: z.string().min(1),
});
const getEnv = makeTypedEnv(serverSchema, { transform: camelKeys });
export const env = () => getEnv(process.env);Caching
By default, validation runs on every call. Enable caching with cache: true to validate only once per args reference:
const getEnv = makeTypedEnv(schema, { cache: true });
getEnv(process.env); // validates
getEnv(process.env); // cached — same reference
getEnv(import.meta.env); // validates — different referenceNote: Caching can cause unexpected behavior with HMR (Hot Module Replacement) during development, since the cached result persists across module reloads. Prefer caching only in production or for server-side code that runs once at startup.
Stripping prefixes
Use replaceKeys from string-ts to strip prefixes like VITE_ before camelCasing:
import { camelKeys, replaceKeys } from "string-ts";
const getEnv = makeTypedEnv(schema, {
transform: (parsed) => camelKeys(replaceKeys(parsed, "VITE_", "")),
});
// VITE_GOOGLE_MAPS_API_KEY → googleMapsApiKeyError messages
When validation fails, you get a clear error listing every issue:
Environment validation failed:
- DATABASE_URL is required
- SESSION_SECRET must be at least 1 characterNo more guessing which variable is missing.
API
makeTypedEnv(schema)
Returns a function (args: Record<string, unknown>) => T that validates args against the schema and returns the parsed result.
makeTypedEnv(schema, options)
| Option | Type | Default | Description |
| ----------- | ------------------ | ------- | ------------------------------------------------------------------------ |
| transform | (parsed: T) => R | — | Transform the parsed result. Return type is inferred. |
| cache | boolean | false | Cache by args reference. Set to true to validate once per args object. |
Errors
- Throws
Errorwith a descriptive message when validation fails - Throws
TypeErrorif an async schema is passed (environment parsing is synchronous)
