objectenvy / objectify
Function: objectify()
Call Signature
function objectify<T>(): T;Defined in: objectEnvy.ts:517
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Type Parameters
| Type Parameter |
|---|
T extends ConfigObject |
Returns
T
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)
Call Signature
function objectify<T, TOut>(options): TOut;Defined in: objectEnvy.ts:519
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Type Parameters
| Type Parameter |
|---|
T extends ZodObject<$ZodLooseShape, $strip> |
TOut extends ConfigObject |
Parameters
| Parameter | Type | Description |
|---|---|---|
options | Omit<ObjectEnvyOptions<output<T>>, "transform"> & { schema: T; transform: (parsed) => TOut; } | Optional configuration controlling prefix, env source, schema, coercion, and nesting. |
Returns
TOut
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)
Call Signature
function objectify<T>(options): output<T>;Defined in: objectEnvy.ts:525
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Type Parameters
| Type Parameter |
|---|
T extends ZodObject<$ZodLooseShape, $strip> |
Parameters
| Parameter | Type | Description |
|---|---|---|
options | ObjectEnvyOptions<output<T>> & { schema: T; } | Optional configuration controlling prefix, env source, schema, coercion, and nesting. |
Returns
output<T>
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)
Call Signature
function objectify<T, TOut>(options): TOut;Defined in: objectEnvy.ts:529
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Type Parameters
| Type Parameter |
|---|
T extends ConfigObject |
TOut extends ConfigObject |
Parameters
| Parameter | Type | Description |
|---|---|---|
options | Omit<ObjectEnvyOptions<T>, "transform"> & { transform: (parsed) => TOut; } | Optional configuration controlling prefix, env source, schema, coercion, and nesting. |
Returns
TOut
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)
Call Signature
function objectify(options): ConfigObject;Defined in: objectEnvy.ts:532
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Parameters
| Parameter | Type | Description |
|---|---|---|
options | Omit<ObjectEnvyOptions<ConfigObject>, "schema" | "env"> & { env?: undefined; } | Optional configuration controlling prefix, env source, schema, coercion, and nesting. |
Returns
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)
Call Signature
function objectify<E>(options): { [KeyType in string | number | symbol]: UnionToIntersection<{ [K in string]: HasSibling<K, keyof E & string> extends true ? BuildNested<K extends `${Head}_${Tail}` ? Head extends "" ? Tail extends `${(...)}_${(...)}` ? (...) extends (...) ? (...) : (...) : [(...)] : [Head, ...((...) extends (...) ? (...) : (...))[]] : [K], CoercedType<E[K]>> : { [P in string]: CoercedType<E[K]> } }[keyof E & string]>[KeyType] };Defined in: objectEnvy.ts:535
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Type Parameters
| Type Parameter |
|---|
E extends EnvLike |
Parameters
| Parameter | Type | Description |
|---|---|---|
options | Omit<ObjectEnvyOptions<ConfigObject>, "schema"> & { env: E; } | Optional configuration controlling prefix, env source, schema, coercion, and nesting. |
Returns
{ [KeyType in string | number | symbol]: UnionToIntersection<{ [K in string]: HasSibling<K, keyof E & string> extends true ? BuildNested<K extends `${Head}_${Tail}` ? Head extends "" ? Tail extends `${(...)}_${(...)}` ? (...) extends (...) ? (...) : (...) : [(...)] : [Head, ...((...) extends (...) ? (...) : (...))[]] : [K], CoercedType<E[K]>> : { [P in string]: CoercedType<E[K]> } }[keyof E & string]>[KeyType] }
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)
Call Signature
function objectify<T>(options): T;Defined in: objectEnvy.ts:539
Parse process.env (or a custom env object) into a strongly-typed, nested, camelCased config object.
Type Parameters
| Type Parameter |
|---|
T extends ConfigObject |
Parameters
| Parameter | Type | Description |
|---|---|---|
options | ObjectEnvyOptions<T> | Optional configuration controlling prefix, env source, schema, coercion, and nesting. |
Returns
T
A nested camelCased config object. Type is inferred from the Zod schema, or from the env source via FromEnv, or falls back to EnviableObject.
Remarks
Without a schema, nesting is determined heuristically: a prefix is nested only when two or more environment variables share it. A single PORT_NUMBER key becomes { portNumber } (flat); two LOG_LEVEL + LOG_PATH keys become { log: { level, path } } (nested). Segments in nonNestingPrefixes (max, min, is, enable, disable by default) are always kept flat.
When a Zod schema is provided, schema structure governs nesting — the heuristic is bypassed — and the parsed output is validated against the schema. An invalid value throws a ZodError.
String values are coerced to number or boolean unless coerce: false is set. Comma-separated strings are parsed into arrays.
Throws
When a Zod schema is provided and the parsed config fails validation.
Use When
- You need to turn raw
process.envinto 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.
Avoid When
- You need per-variable access with
.required()/.asInt()semantics — useenv-varinstead. - 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:.
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_CASEenv object when relying onFromEnvtypes — 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'becomes1(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; mutatingprocess.envafter caching returns stale data.
Examples
// Smart nesting — only nests when multiple entries share a prefix
// PORT_NUMBER=1234 LOG_LEVEL=debug LOG_PATH=/var/log
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env });
// { portNumber: 1234, log: { level: 'debug', path: '/var/log' } }
// portNumber is flat (only one PORT_* entry); log is nested (multiple LOG_* entries)// With prefix filtering
// APP_PORT=3000 APP_DEBUG=true OTHER_VAR=ignored
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, prefix: 'APP' });
// { port: 3000, debug: true }// With Zod schema for validation and guaranteed structure
import { objectify } from 'objectenvy';
import { z } from 'zod';
const schema = z.object({
portNumber: z.number(),
log: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: z.string()
})
});
const config = objectify({ env: process.env, schema });
// Throws ZodError if PORT_NUMBER is missing or LOG_LEVEL is not a valid enum value// Disable coercion to keep all values as strings
import { objectify } from 'objectenvy';
const config = objectify({ env: process.env, coerce: false });
// { port: '3000', debug: 'true' } — no type conversion appliedSee
- objectEnvy for a memoized factory wrapper
- envy for the inverse operation (config → env)