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>;
}envFilesdefaults to[]. Files are read synchronously in array order; later files win. A string path is required. An object path is skipped only whenoptional: trueand the file is missing; other read failures becomesource_read_failedissues.sourcedefaults toprocess.env. Supplying any record, including{}, disables ambient lookup. Values must be strings orundefined.overridesdefaults 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 objectProperty 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(...)orvalue.toString()rather than theObject.*equivalents. console.logandutil.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 | undefinedGeneratedSchema.protocolVersion must be 1. Manually authored schemas can
throw ConfigError with incompatible_schema; no compatibility is promised
for invented schema shapes.
Runtime API
The symbols exported from the typespun runtime package — ConfigError and ConfigIssue — with their exact shapes and the meaning of every issue field.
CLI reference
The typespun-codegen commands — init, generate and check — with their flags, exact console output, and the exit codes CI should rely on.