Skip to content

objectenvy / objectify

Function: objectify()

Call Signature

ts
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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Call Signature

ts
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

ParameterTypeDescription
optionsOmit<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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Call Signature

ts
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

ParameterTypeDescription
optionsObjectEnvyOptions<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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Call Signature

ts
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

ParameterTypeDescription
optionsOmit<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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Call Signature

ts
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

ParameterTypeDescription
optionsOmit<ObjectEnvyOptions<ConfigObject>, "schema" | "env"> & { env?: undefined; }Optional configuration controlling prefix, env source, schema, coercion, and nesting.

Returns

ConfigObject

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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Call Signature

ts
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

ParameterTypeDescription
optionsOmit<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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Call Signature

ts
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

ParameterTypeDescription
optionsObjectEnvyOptions<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.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.

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:.

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.

Examples

ts
// 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)
ts
// 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 }
ts
// 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
ts
// 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 applied

See

  • objectEnvy for a memoized factory wrapper
  • envy for the inverse operation (config → env)

Released under the MIT License.