Promote literal-discriminated unions to a first-class kind#22
Merged
guybedford merged 1 commit intomainfrom May 5, 2026
Merged
Promote literal-discriminated unions to a first-class kind#22guybedford merged 1 commit intomainfrom
guybedford merged 1 commit intomainfrom
Conversation
A type alias `type Foo = A | B | …` whose branches share a required, literal-typed property now lands in a new `TypeKind::DiscriminatedUnion` rather than merging into a flat `InterfaceDecl`. The discriminator can be a string, number, or boolean literal — the three forms TS narrows on. The motivation: the merged-shape model erased per-branch required-vs-optional information. `EmailAttachment.contentId` is required in the `inline` branch and absent (`?: undefined`) in the `attachment` branch; the merge marked it optional everywhere, so `new_inline(...)` didn't take it as a parameter even though the source contract requires it. The codegen now keeps a per-branch view: the extern block stays merged (one getter/setter set per property across branches), the `impl` emits one `new_<discriminator>` / `builder_<discriminator>` cohort per branch driven by that branch's required set, and the wrapper builder still exposes the merged-optional fluent setters (callers going through `builder_attachment(...).content_id(x).build()` aren't blocked — the branch invariant is enforced at the `new_<discriminator>` boundary). `generate_dictionary_factory` was refactored to take an optional list of required-field passes; plain interfaces / dictionaries pass nothing and behave as before, discriminated unions thread one pass per branch. The shared `emitted_names` set deduplicates combos that converge across passes, and a per-pass `pass_covers_optionals` check skips the `builder_<branch>` companion when a branch already covers every merged-optional field — so `new_inline(content_id, …)` (which covers the only optional) gets an inlined body, while `new_attachment(…)` (which leaves `contentId` to the wrapper) keeps its `builder_attachment` companion. Snapshot updates are limited to the dictionaries that previously landed alphabetical (now source-order, see #21) and the discriminated unions: `EmailAttachment`, `KVNamespaceListResult`, `ReadableStreamReadResult`, `Status`, `D1Response`, …. Plain interfaces are unchanged.
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.
A type alias
type Foo = A | B | …whose branches share a required, literal-typed property now lands in a newTypeKind::DiscriminatedUnionrather than merging into a flatInterfaceDecl. The discriminator can be a string, number, or boolean literal — the three forms TS narrows on.The motivation is per-branch required-vs-optional accuracy. With the previous merged-shape model,
EmailAttachment.contentId(required in theinlinebranch,?: undefinedinattachment) collapsed to merged-optional, sonew_inline(...)didn't take it as a parameter even though the source contract requires it:The codegen keeps a per-branch view:
pub type Fooplus a getter/setter set per property across all branches. No change for downstream consumers reaching for the bare type.impl Fooemits onenew_<discriminator>/builder_<discriminator>cohort per branch, driven by that branch's required set. Each branch independently goes through the literal-collapse / value-union expansion /_with_<type>suffixing rules.EmailAttachmentBuilder::content_id(self, val)is always available so callers going throughbuilder_attachment(...).content_id(x).build()aren't blocked. The branch invariant is enforced at thenew_<discriminator>boundary; once past that, the wrapper exposes the runtime-permissive view of the type.builder_<branch>suppression: when a branch's required set already covers every merged-optional field, thebuilder_<branch>companion would have nothing to chain — sonew_<branch>is emitted with an inlined body and no builder. ForEmailAttachment,inlinecovers the only merged-optional (contentId) so it gets an inlined body, whileattachmentleavescontentIdfor the wrapper and keeps itsbuilder_attachmentcompanion.Implementation:
parse/literal_union.rs::detect_discriminators— a property qualifies when it's required and string/number/boolean-literal-typed in every branch.parse/first_pass/populate.rs::try_synthesize_alias_decl— returnsTypeKind(wasInterfaceDecl); routes toDiscriminatedUnionwhen discriminators are detected, plainInterfaceotherwise.codegen/classes.rs—generate_dictionary_factoryis now a thin wrapper aroundgenerate_dictionary_factory_with_passes, which takes an optional list of required-field passes. Plain interfaces pass nothing and behave as before; the new module passes one entry per branch. A sharedemitted_namesset deduplicates combos that converge across passes, and the per-passpass_covers_optionalscheck drives the newbuilder_<branch>suppression.codegen/discriminated_unions.rs— new module wiringDiscriminatedUnionDeclthrough the shared infra.Snapshot diffs are limited to discriminated unions and dictionaries that previously landed alphabetical (now source-order, per #21). Plain interfaces are unchanged.
CONVENTIONS.mdgains a## Discriminated unionssection spelling out the detection rule, the per-branch factory shape, the wrapper-stays-merged invariant, and thebuilder_<branch>suppression rule.