| name | objectenvy |
| description | Automatically map process.env to strongly-typed, nested config objects with camelCase fields Use when working with config, configuration, env, environment, type-safe, zod, nested, camelcase. |
| license | MIT |
objectenvy
Automatically map process.env to strongly-typed, nested config objects with camelCase fields
When to Use
- Working with config, configuration, env, environment, type-safe, zod, nested, camelcase
- You need to turn raw
process.env into a typed, nested config object at application startup.
- You have a Zod schema and want validated, fully-typed config in a single call.
- You want to scope config to one namespace using
prefix: 'APP' and strip the prefix from keys.
- You use double-underscore env naming (
LOG__LEVEL) and want { log: { level } } nesting.
- You have a single canonical app-config module and want to read config exactly once per process lifecycle.
- You need to inject a different
env object in tests while keeping the same schema and prefix.
- You want a named handle that bundles both directions of the round-trip (
objectify + envy).
- You need to spawn a child process and want to pass typed config as env variables.
- You're writing a
.env file from a config object (e.g., for CI scaffolding or test fixtures).
- You use
ToEnv<T> for compile-time validation and need the runtime values to match.
- You're round-tripping:
objectify() → mutate config → envy() → write back to env.
- You want to layer environment config on top of hard-coded application defaults.
- You have partial user-supplied configs and need safe fallback values for unset fields.
- You're building a plugin or middleware layer that injects sensible defaults without overriding user intent.
- You need to combine two configuration objects where neither is the authoritative "defaults" — e.g.,
- merging a base config with a feature-flag overlay.
- You're composing multiple partial config slices loaded from different sources.
- You need array concatenation across config layers (
concat or concat-unique).
- You need to convert a raw environment variable key to a JavaScript property name.
- You're normalising keys before building a config object.
- You need to convert a camelCase config key to an env variable name for
envy().
- You're generating
.env documentation or scaffolding from TypeScript property names.
- You want to apply the same type-coercion rules that
objectify() uses internally to an individual value.
- You're processing env values outside
objectify() and need consistent boolean/number parsing.
Avoid when:
- You need per-variable access with
.required() / .asInt() semantics — use env-var instead.
- You already have a fully validated config object and just want to merge defaults — use
override().
- You need multiple env sources (files + remote secrets) — load them first, then pass as
env:.
- You need a fresh re-read on every call (e.g., dynamic secrets) — memoization will return stale data.
- You use different schemas in different parts of the app — create separate
objectEnvy instances instead.
- You only need the
ToEnv<T> type at compile time — no need to call envy() at runtime.
- The config contains
Date, Map, Set, or class instances — envy() serializes them as
[object Object] via String().
- You need a symmetric deep merge where neither object has priority — use
merge() instead.
- You need to merge more than two objects at once — chain multiple
override() calls.
- You want one object to be authoritative "defaults" and the other to win — use
override() instead.
- You need to merge more than two objects — chain
merge(merge(a, b), c) calls.
- Input may contain non-ASCII letters — the regex captures only
[a-z] after the underscore.
- You need
PascalCase output — capitalise the first character of the result separately.
- You need a strictly reversible transform —
toCamelCase(toSnakeCase('apiURL')) yields 'apiUrl',
- not
'apiURL'.
- The value must stay a string regardless of content (e.g.,
'123' must stay '123') — pass
coerce: false to objectify() instead, or handle the type downstream.
- You need locale-aware number parsing —
parseFloat/parseInt are locale-independent but only
- handle decimal notation; scientific notation (
'1e5') is NOT coerced to a number.
- API surface: 8 functions, 8 types
Pitfalls
- NEVER rely on heuristic nesting for shared prefixes in production — BECAUSE adding a second
PORT_* variable later silently restructures { portNumber } into { port: { number } },
- breaking all downstream key accesses without a type error at the call site. Prefer a Zod schema.
- NEVER pass a non-
SCREAMING_SNAKE_CASE env object when relying on FromEnv types — BECAUSE
- the type utility assumes keys are uppercase snake_case; mixed-case keys produce incorrect types.
- NEVER use
coerce: true (the default) if a value looks like a number but must stay a string —
- BECAUSE
'01' becomes 1 (integer parse), losing the leading zero.
- NEVER pass a mutable reference to the cached env when using
objectEnvy() — BECAUSE the
- WeakMap cache key is the object reference; mutating
process.env after caching returns stale data.
- NEVER mutate
process.env after calling the inner objectify() expecting the result to update —
- BECAUSE results are cached by WeakMap keyed on the env object reference; the cached value is returned.
- NEVER share one
objectEnvy instance across packages that need independent schemas — BECAUSE the
- schema is baked into the instance at creation time and cannot be changed per call.
- NEVER rely on
envy() to round-trip arrays of objects faithfully — BECAUSE object items are
JSON.stringify-ed then joined; when objectify() re-reads the comma-separated string, it
- treats it as a string array, not an array of objects.
- NEVER pass
null or undefined values in the config — BECAUSE envy() silently skips
null/undefined entries, leaving no env key for them; the round-trip loses those fields.
- NEVER expect
envy() to honour a prefix — BECAUSE it outputs bare SCREAMING_SNAKE_CASE keys
- with no prefix. Add the prefix yourself if your deployment expects
APP_PORT rather than PORT.
- NEVER mutate the
defaults or config arguments after calling override() — BECAUSE the
- returned object is a shallow copy at each level; nested sub-objects are NOT deep-cloned, so
- mutations to deeply nested objects propagate back through the shared reference.
- NEVER rely on
override() to handle class instances or special objects (Date, Map, Set) — BECAUSE
- the function checks
typeof === 'object' and recurses, which may produce unexpected results for
- non-plain-object values.
- NEVER rely on
merge() to deep-clone the inputs — BECAUSE nested sub-objects are shallow-copied
- at each level, so mutations to deeply nested objects in the result affect the originals.
- NEVER use
'concat-unique' to deduplicate object items if equality matters beyond JSON serialisation —
- BECAUSE the implementation uses
JSON.stringify for comparison, which is order-sensitive and ignores
undefined values, Date objects, and prototype methods.
- NEVER assume
merge() handles non-plain objects (Map, Set, Date, class instances) correctly —
- BECAUSE the function checks
typeof === 'object' and recurses, producing incorrect results for
- these types.
- NEVER assume
toCamelCase(toSnakeCase(x)) === x for all inputs — BECAUSE acronym boundaries
- (e.g.,
apiURL → API_URL → apiUrl) collapse consecutive capitals, so the round-trip is
- lossy for strings with adjacent uppercase letters.
- NEVER assume acronym round-trips are lossless — BECAUSE
getHTTPSUrl → GET_HTTPS_URL →
getHttpsUrl, losing the original casing of consecutive uppercase letters.
- NEVER feed already-snake-cased input — BECAUSE
PORT_NUMBER → PORT__NUMBER (double underscore)
- due to the camelCase split regex firing on the
_N boundary.
- NEVER use
coerceValue on values that use commas as decimal separators (e.g., '3,14' in
- some locales) — BECAUSE the function will treat this as an array
[3, 14] rather than the
- float
3.14.
- NEVER pass leading-zero strings you want preserved as strings (e.g., zip codes
'01234') —
- BECAUSE the integer regex matches and
parseInt('01234', 10) returns 1234.
- NEVER rely on
'on'/'off' being coerced to booleans — BECAUSE only true/false/yes/no/y/n
- are in the boolean equivalents set;
'on' stays as the string 'on'.
Configuration
2 configuration interfaces — see references/config.md for details.
- ObjectEnvyOptions — Configuration options for
objectify() — controls prefix filtering,
env source, Zod schema validation, camelCase nesting behaviour, and include/exclude patterns.
- MergeOptions — Options for controlling the merge behaviour of
merge() and override().
Quick Reference
Parsing: objectify, objectEnvy, toCamelCase, coerceValue
Serialization: envy, toSnakeCase
Merging: override, merge
Type Utilities: ConfigObject, ConfigValue, ArrayMergeStrategy, ToEnv, FromEnv, WithPrefix, WithoutPrefix, SchemaToEnv
Links