Typespun
Reference

Generated loader API

What every generated module exports, the full LoadConfigOptions contract for envFiles, source and overrides, and the typespun/generated ABI the emitted file calls.

Every generated module exposes exactly the application-facing type and loader:

export type Config = /* selected root */;
export const loadConfig: (options?: LoadConfigOptions<Config>) => Config;

Import them from the generated file, not from typespun:

import { loadConfig, type Config } from './generated/typespun.js';

const config: Config = loadConfig({ source: process.env });

LoadConfigOptions<T>

Defined in typespun/generated and inferred by loadConfig():

interface LoadConfigOptions<T> {
  readonly envFiles?: readonly (
    | string
    | { readonly path: string; readonly optional?: boolean }
  )[];
  readonly source?: Readonly<Record<string, string | undefined>>;
  readonly overrides?: DeepPartial<T>;
}
  • envFiles defaults to []. Files are read synchronously in array order; later files win. A string path is required. An object path is skipped only when optional: true and the file is missing; other read failures become source_read_failed issues.
  • source defaults to process.env. Supplying any record, including {}, disables ambient lookup. Values must be strings or undefined.
  • overrides defaults to none. Values are already typed, recursively merged by leaf, and validated without coercion. Unknown, unsafe, or cyclic paths fail.

The call is synchronous and returns a newly reconstructed object. Resolved arrays are shallow-cloned. It throws ConfigError when issues exist.

The returned object

loadConfig() returns a newly reconstructed object whose every level has a null prototype, including nested branches. This is deliberate: a configuration object is built from external input, and a null prototype means a key such as __proto__ arriving from the environment cannot reach Object.prototype.

The practical consequence is that inherited methods are not available:

const config = loadConfig();

Object.getPrototypeOf(config.server); // null
config.server.hasOwnProperty('port'); // TypeError: not a function

// These all work:
Object.hasOwn(config.server, 'port'); // true
JSON.stringify(config.server); // {"host":"127.0.0.1","port":3000}
const copy = { ...config.server }; // ordinary object

Property access, spreading, JSON.stringify, Object.keys and Object.hasOwn behave normally, so most code never notices. Two things do:

  • Libraries that duck-type with value.hasOwnProperty(...) or value.toString() rather than the Object.* equivalents.
  • console.log and util.inspect, which render the object as [Object: null prototype] { ... }.

If you need a conventional object, spread it: { ...config } produces one with Object.prototype, though only at the level you spread.

Generated ABI

The typespun/generated entry point also exports createLoader<T>(schema), resolveConfig<T>(schema, options?), and validateTypedValue(kind, value), plus DeepPartial, FieldKind, FieldSchema, GeneratedSchema, and LoadConfigOptions. These are public package exports for generated-code compatibility; application code should normally use its generated module.

createLoader<T>(schema: GeneratedSchema):
  (options?: LoadConfigOptions<T>) => T

resolveConfig<T>(schema: GeneratedSchema,
  options?: LoadConfigOptions<T>): T

validateTypedValue(kind: FieldKind, value: unknown): string | undefined

GeneratedSchema.protocolVersion must be 1. Manually authored schemas can throw ConfigError with incompatible_schema; no compatibility is promised for invented schema shapes.

On this page