objectenvy / FromEnv
Type Alias: FromEnv<T, D>
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 Parameter | Default type | Description |
|---|---|---|
T | - | A flat env record type with SCREAMING_SNAKE_CASE keys. |
D extends Depth | 5 | Internal 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_NUMBER → portNumber).
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 verifyobjectify()output shape. - You want the config type inferred from a
satisfies EnvLikeenv object.
Avoid When
- The env object has dynamic (non-literal) keys —
FromEnv<Record<string, string>>yieldsRecord<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
FromEnvfor nesting when keys share a prefix that is innonNestingPrefixes— BECAUSE the type utility does not know aboutnonNestingPrefixes; the runtime and type may disagree forMAX_*,IS_*, etc. prefixes. - NEVER pass a
process.envtype directly — BECAUSEprocess.envisNodeJS.ProcessEnv(Record<string, string | undefined>), which has no literal keys;FromEnvneeds aconst-asserted or explicitly typed object to produce useful output.
Example
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