Skip to content

objectenvy / objectEnvy

Function: objectEnvy()

Call Signature

ts
function objectEnvy(defaultOptions): {
  envy: <T>(config) => { [KeyType in string | number | symbol]: UnionToIntersection<[T] extends [unknown[]] ? never : [T] extends [object] ? { [K in string]: [(...)] extends [(...)] ? (...) extends (...) ? (...) : (...) : (...) extends (...) ? (...) : (...) }[keyof T & string] : [T] extends [Primitive] ? never : never>[KeyType] };
  objectify: (overrides?) => ConfigObject;
};

Defined in: objectEnvy.ts:672

Create a memoized configuration loader with preset options, returning bound objectify and envy helpers.

Parameters

ParameterTypeDescription
defaultOptionsOmit<ObjectEnvyOptions, "schema">Default options applied to every inner objectify() call. Schema is fixed per instance; it cannot be overridden in the inner calls.

Returns

An object with a memoized objectify(overrides?) and the envy converter.

NameTypeDefined in
envy()<T>(config) => { [KeyType in string | number | symbol]: UnionToIntersection<[T] extends [unknown[]] ? never : [T] extends [object] ? { [K in string]: [(...)] extends [(...)] ? (...) extends (...) ? (...) : (...) : (...) extends (...) ? (...) : (...) }[keyof T & string] : [T] extends [Primitive] ? never : never>[KeyType] }objectEnvy.ts:674
objectify()(overrides?) => ConfigObjectobjectEnvy.ts:673

Remarks

objectEnvy acts as a factory: call it once at module load time with your default options (prefix, schema, delimiter, etc.) and it returns a pair of functions. The inner objectify is memoized per env-object reference and option-set combination, so repeated calls within the same process return the same config instance without re-parsing. Pass { env: testEnv } to the inner objectify to override the env source for unit testing without polluting module-level state.

Use When

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

Avoid When

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

Pitfalls

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

Examples

ts
// Module-level config singleton with Zod schema
import { objectEnvy } from 'objectenvy';
import { z } from 'zod';

const schema = z.object({ port: z.number(), debug: z.boolean() });
const { objectify: loadConfig, envy: toEnv } = objectEnvy({ prefix: 'APP', schema });

export const config = loadConfig();            // memoized; reads process.env once
export const rawEnv = toEnv(config);           // convert back to env format
ts
// Override env for unit tests
import { objectEnvy } from 'objectenvy';
const { objectify } = objectEnvy({ prefix: 'APP' });
const testConfig = objectify({ env: { APP_PORT: '9000', APP_DEBUG: 'true' } });

See

  • objectify for the stateless version without memoization
  • envy for converting config objects back to env format

Call Signature

ts
function objectEnvy<T>(defaultOptions): {
  envy: <T>(config) => { [KeyType in string | number | symbol]: UnionToIntersection<[T] extends [unknown[]] ? never : [T] extends [object] ? { [K in string]: [(...)] extends [(...)] ? (...) extends (...) ? (...) : (...) : (...) extends (...) ? (...) : (...) }[keyof T & string] : [T] extends [Primitive] ? never : never>[KeyType] };
  objectify: (overrides?) => T;
};

Defined in: objectEnvy.ts:676

Create a memoized configuration loader with preset options, returning bound objectify and envy helpers.

Type Parameters

Type Parameter
T extends ConfigObject

Parameters

ParameterTypeDescription
defaultOptionsObjectEnvyOptions<T> & { schema: ZodObject<any, $strip> | T; }Default options applied to every inner objectify() call. Schema is fixed per instance; it cannot be overridden in the inner calls.

Returns

An object with a memoized objectify(overrides?) and the envy converter.

NameTypeDefined in
envy()<T>(config) => { [KeyType in string | number | symbol]: UnionToIntersection<[T] extends [unknown[]] ? never : [T] extends [object] ? { [K in string]: [(...)] extends [(...)] ? (...) extends (...) ? (...) : (...) : (...) extends (...) ? (...) : (...) }[keyof T & string] : [T] extends [Primitive] ? never : never>[KeyType] }objectEnvy.ts:680
objectify()(overrides?) => TobjectEnvy.ts:679

Remarks

objectEnvy acts as a factory: call it once at module load time with your default options (prefix, schema, delimiter, etc.) and it returns a pair of functions. The inner objectify is memoized per env-object reference and option-set combination, so repeated calls within the same process return the same config instance without re-parsing. Pass { env: testEnv } to the inner objectify to override the env source for unit testing without polluting module-level state.

Use When

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

Avoid When

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

Pitfalls

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

Examples

ts
// Module-level config singleton with Zod schema
import { objectEnvy } from 'objectenvy';
import { z } from 'zod';

const schema = z.object({ port: z.number(), debug: z.boolean() });
const { objectify: loadConfig, envy: toEnv } = objectEnvy({ prefix: 'APP', schema });

export const config = loadConfig();            // memoized; reads process.env once
export const rawEnv = toEnv(config);           // convert back to env format
ts
// Override env for unit tests
import { objectEnvy } from 'objectenvy';
const { objectify } = objectEnvy({ prefix: 'APP' });
const testConfig = objectify({ env: { APP_PORT: '9000', APP_DEBUG: 'true' } });

See

  • objectify for the stateless version without memoization
  • envy for converting config objects back to env format

Released under the MIT License.