Repository navigation
fix: unify TypeScript untagged-union conversion - #170
Merged
adamcavendish merged 1 commit intoNov 10, 2026
Merged
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
adamcavendish
force-pushed
the
adamcavendish/typescript-untagged-union-conversion
branch
3 times, most recently
from
November 10, 2026 15:32
9218fbc to
d0f9b04
Compare
adamcavendish
force-pushed
the
adamcavendish/typescript-untagged-union-conversion
branch
from
November 10, 2026 18:34
d0f9b04 to
4e31d73
Compare
- Plan public and wire types, export symbols, and runtime effects once for models, HTTP payloads, multipart inputs, and tagged payloads and guards. - Retain nested scalar enum constraints at their original nullable boundaries without treating synthetic type fallbacks as constraints or changing other generators' existing representations and codec policies. - Count original concrete candidates before fallbacks, delegate once, and retain authored null matches and nested strict errors through properties and containers. - Keep raw union subtrees wire-typed without disabling ordinary parent conversion or weakening readonly dictionary and nested array contracts. - Emit static internal conversion and reachable matching helpers with structured Vite+-stable layout, retaining full checks on untouched output before and after packing, isolated model dependencies, and no public barrel helper exports. - Use published sigil-stitch 0.7.5 for qualified utility types with tracked nested imports, preserving models named Record or Readonly and their converter names. - Distinguish omitted JSON bodies from supplied null in required and optional requests, restoring nullable wrappers without erasing inner finite constraints; retain native consumer, declaration, exact render and golden contracts.
adamcavendish
force-pushed
the
adamcavendish/typescript-untagged-union-conversion
branch
from
November 10, 2026 21:09
4e31d73 to
9db7a0a
Compare
adamcavendish
deleted the
adamcavendish/typescript-untagged-union-conversion
branch
November 10, 2026 21:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Convert camelCase TypeScript untagged unions through one value plan shared by
models, HTTP payloads, multipart inputs, exports, and tagged payloads/type guards.
Mixed client/search tools consistently use
input_schemaandmax_useson thewire, including through aliases and nested containers.
Problem
Independent type and conversion walks disagreed about inline unions, aliases,
and container depth. One convertibility flag conflated renamed types with
runtime checks: identical
integer | numberrepresentations could skip strictambiguity checks. Raw aliases could promise ergonomic types while preserving
wire keys instead.
Public/wire representations, required symbols, and runtime effects are separate
facts. Scalar enum constraints survive all supported OpenAPI lowerers; TypeScript
does not recover them by rereading raw specifications.
Behavior
typescript-fetch.untagged_union_strategycontrols camelCase union conversion:firstMatch(default)strictrawMatching checks required own fields, scalar literals/enums, array and dictionary
values, and typed extensions separately from declared fields. Nested finite
constraints intersect at their original nullability boundaries; an outer enum
does not replace an inner enum or widen its values. Untyped scalar enums match
their finite values without inheriting a synthetic string constraint from the
legacy type projection. Selection is pure:
count/select first, then invoke one original delegate. Inner strict errors
propagate without retrying another member.
Repeated nullable wrappers do not create ambiguity or promote opaque fallbacks.
Authored concrete null matches beside unrestricted fallbacks remain visible to
outer selection. Null already accepted by an inner union still reaches that
union through properties and containers. An outer wrapper can admit null without
calling a non-nullable inner converter. Enum metadata restores an extracted
nullable conversion wrapper rather than passing null to that nonnullable
delegate; already-nullable inner unions still receive null and run strict checks.
Inner finite constraints stay inside that restored wrapper, preserving outer-only
null permission consistently between properties and array items.
Productive recursion and compound conversions use static internal functions,
not a runtime schema interpreter or guards weakened to unrestricted JSON. Only
matchers reachable from union checks are emitted. Helpers are not re-exported
by public barrels. Open dictionaries retain concrete conversion alongside opaque
entries; empty/entirely opaque dictionaries remain fallbacks.
Conversion helper modules retain their own model/child-helper dependencies, so
importing a usable model does not pull unrelated models into its consumer.
Raw subtrees stay wire-typed while ordinary parents and siblings still convert.
Singleton
allOfaliases preserve arrays/null and readonly dictionaries. Thesame projections handle nested JSON requests, successful and lazy error
responses, JSON multipart parts, and direct tagged payloads/guards. A required
schema-nullable JSON body accepts supplied null but rejects omission. Optional
JSON bodies skip conversion only for
undefined; supplied null retains the samestrict checks as direct or required conversion. Multipart preserves supplied
upload filenames, binary bytes and omitted optional parts.
Compatibility and boundaries
Generated converter names and readonly contracts are retained, including the
existing readonly-outer/plain-inner nested array contract. Preserve mode adds no
JSON converters. Helper inputs accept readonly arrays without widening public
API results. Imports, declarations, guards and expressions are laid out directly
for full Vite+ checks before and after packing; no formatter preparation or
generated-source compatibility patch is needed. CI bootstrap follows Vite+ 1.5.1
and TypeScript 7.0.2.
Published sigil-stitch 0.7.5 supplies semantic qualified utility types: models
named
RecordorReadonlykeep their public names, while built-in referencesuse
globalThis.RecordorglobalThis.Readonly. Open external declarations andguards share their wrapper type, retaining nested imports without raw types.
The other eight generators consume retained legacy representations rather than
adopting a new enum or serialization policy. Rust library consumers exhaustively
matching
IrTypeExprmust handleScalarEnum, including itsrepresentation_is_constraintfact distinguishing authored constraints fromlegacy fallbacks and
representation_nullablerecording where propertynullability was extracted. Exhaustive
TypeScriptFetchConfigliterals initializeuntagged_union_strategyor use..Default::default().Matching is structural conversion dispatch, not full JSON Schema validation of
formats, bounds or every keyword. Multi-member
allOfcomposition is notredesigned. Same-value circular aliases remain outside this change and can still
cause
TS2456; exploratory consumers do not claim full-barrel compilation whentheir fixtures contain those unsupported aliases. No subprocess-driven test
harness, Python tooling/union repair, warning suppression, release change or
publication is introduced.