Skip to content

Expose post-parse config types so defaulted fields are not typed as optional #18

Description

@taras

Summary

When we infer config types from configliere field maps, fields with schema defaults are still typed as optional (T | undefined) even though parsing guarantees they are present at runtime. This forces downstream code to add assertion helpers like assertDefault() just to convince TypeScript of invariants the parser already knows.

Problem

We currently have code like this in downstream consumers:

type RunParsedConfig = InferConfig<typeof runFields>;

where runFields contains defaults such as:

"show-browser": field(z.boolean().optional().default(false))
"timeout-immediate": field(z.number().optional().default(2000))

At runtime, after parse(...), these fields are present. But at the type level they remain:

  • "show-browser"?: boolean
  • "timeout-immediate"?: number

That mismatch forces code like:

'show-browser': assertDefault(config["show-browser"], "show-browser"),
'timeout-immediate': assertDefault(config["timeout-immediate"], "timeout-immediate"),

This is not a real runtime defaulting step. It is only a type-level assertion to bridge a gap between parser behavior and exposed types.

Why this is a problem

  • Downstream code has to re-assert parser guarantees
  • Conversion layers become noisy and misleading
  • Defaults are defined in one place but effectively "re-proven" elsewhere
  • Handwritten normalized config types can drift from field definitions
  • Users of configliere lose confidence that parser output types reflect actual parse semantics

Desired behavior

After parsing, fields with defaults should be reflected in the output type as required values.

For example, if a parser contains:

field(z.boolean().optional().default(false))
field(z.number().optional().default(2000))

then the parsed output type should expose:

  • boolean, not boolean | undefined
  • number, not number | undefined

Ideal API direction

Any of these would solve the problem:

  1. A parser-output type that reflects post-parse guarantees

    Example shape:

    const parser = object(runFields);
    type Parsed = InferParsed<typeof parser>;
  2. Stronger typing on parse(...).value so defaults are already represented in the result type

  3. A normalization/output helper type exported by configliere that represents the schema after defaults are applied

Concrete downstream benefit

This would let downstream code simplify from:

'show-browser': assertDefault(config["show-browser"], "show-browser"),
'timeout-immediate': assertDefault(config["timeout-immediate"], "timeout-immediate"),

to:

'show-browser': config["show-browser"],
'timeout-immediate': config["timeout-immediate"],

and eliminate assertion helpers that exist only because the types are weaker than the runtime semantics.

Example use case

A CLI bootstrap parses command config, then passes it through a conversion layer into legacy command option types. Some fields are truly required by policy and still need validation (requireEnv()), but fields with parser defaults should not require additional assertions.

Expected outcome

Consumers should be able to rely on parser output types as the source of truth for:

  • Requiredness after defaults
  • Normalized scalar/array shape
  • Downstream option conversion without assertion helpers for defaulted fields

Related context

This is closely related to the general goal of making configliere parser output types better reflect real parser behavior, especially for defaults and other normalization steps.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions