Skip to content

objectenvy / FromEnv

Type Alias: FromEnv<T, D>

ts
type FromEnv<T, D> = Simplify<UnionToIntersection<{ [K in keyof T & string]: HasSibling<K, keyof T & string> extends true ? BuildNested<SplitKey<K, D>, CoercedType<T[K]>> : { [P in CamelCase<Lowercase<K>>]: CoercedType<T[K]> } }[keyof T & string]>>;

Defined in: typeUtils.ts:195

Convert a flat SCREAMING_SNAKE_CASE env record type to a nested camelCase config type.

Type Parameters

Type ParameterDefault typeDescription
T-A flat env record type with SCREAMING_SNAKE_CASE keys.
D extends Depth5Internal recursion depth counter (do not set manually).

Remarks

FromEnv examines the keys of T and applies smart nesting: if two or more keys share the same first segment (e.g., LOG_LEVEL and LOG_PATH share LOG), those keys are grouped under a nested object (log: { level, path }). Keys without siblings remain flat with the full key camelCased (PORT_NUMBERportNumber).

Value types are passed through CoercedType: literal BooleanString unions become boolean, literal NumberString values become number, and other strings stay as-is.

Recursion is capped at depth 5. The type is a compile-time companion to the runtime behaviour of objectify() — the two should produce equivalent shapes for well-formed env objects.

Use When

  • You have a const-asserted env literal and want to derive the config type without a Zod schema.
  • You're writing type tests (expectTypeOf) to verify objectify() output shape.
  • You want the config type inferred from a satisfies EnvLike env object.

Avoid When

  • The env object has dynamic (non-literal) keys — FromEnv<Record<string, string>> yields Record<string, string> rather than a useful nested type.
  • You need validated values (not just typed) — use a Zod schema with objectify() instead.

Pitfalls

  • NEVER rely on FromEnv for nesting when keys share a prefix that is in nonNestingPrefixes — BECAUSE the type utility does not know about nonNestingPrefixes; the runtime and type may disagree for MAX_*, IS_*, etc. prefixes.
  • NEVER pass a process.env type directly — BECAUSE process.env is NodeJS.ProcessEnv (Record<string, string | undefined>), which has no literal keys; FromEnv needs a const-asserted or explicitly typed object to produce useful output.

Example

ts
import type { FromEnv } from 'objectenvy';

// Flat keys stay flat
type Env1 = { PORT_NUMBER: string };
type Config1 = FromEnv<Env1>;
// { portNumber: string }

// Shared prefix triggers nesting
type Env2 = { LOG_LEVEL: string; LOG_PATH: string };
type Config2 = FromEnv<Env2>;
// { log: { level: string; path: string } }

See

ToEnv for the inverse transformation

Released under the MIT License.