objectenvy / objectEnvy
Function: objectEnvy()
Call Signature
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
| Parameter | Type | Description |
|---|---|---|
defaultOptions | Omit<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.
| Name | Type | Defined 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?) => ConfigObject | objectEnvy.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
envobject 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
objectEnvyinstances instead.
Pitfalls
- NEVER mutate
process.envafter calling the innerobjectify()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
objectEnvyinstance across packages that need independent schemas — BECAUSE the schema is baked into the instance at creation time and cannot be changed per call.
Examples
// 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// 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
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
| Parameter | Type | Description |
|---|---|---|
defaultOptions | ObjectEnvyOptions<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.
| Name | Type | Defined 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?) => T | objectEnvy.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
envobject 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
objectEnvyinstances instead.
Pitfalls
- NEVER mutate
process.envafter calling the innerobjectify()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
objectEnvyinstance across packages that need independent schemas — BECAUSE the schema is baked into the instance at creation time and cannot be changed per call.
Examples
// 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// Override env for unit tests
import { objectEnvy } from 'objectenvy';
const { objectify } = objectEnvy({ prefix: 'APP' });
const testConfig = objectify({ env: { APP_PORT: '9000', APP_DEBUG: 'true' } });