Skip to content

fix(generate): close the unreachable arm of a single-tag variant conversion - #189

Closed
marc0olo wants to merge 1 commit into
fix/nested-option-payloadsfrom
fix/single-tag-variant
Closed

marc0olo wants to merge 1 commit into
fix/nested-option-payloadsfrom
fix/single-tag-variant

Conversation

@marc0olo

Copy link
Copy Markdown
Contributor

Stacked on #188.

Behaviour

variant { p : opt nat } as an argument before after
generated conversion value.__kind__ === "p" ? { p: … } : value — the fallthrough returns the input where the candid shape is expected, TS2322 under the stripped header … : value as never
runtime and wire unchanged unchanged — the arm is erased and is unreachable for a well-formed value
variant { p : opt nat; z } typechecks: after the last test TypeScript has narrowed the input to never unchanged

Four real interfaces carry a single-tag variant in argument position and typecheck clean now: the LLM canister, SNS-W and the ckETH minter among them. No existing snapshot moves.

Why

The fallthrough arm relies on TypeScript narrowing the discriminated union to never once every tag has been tested. That narrowing only happens on a union; a variant with one tag lowers to a single object type, so the arm keeps the input type. unreachable_value already asserts never in the one other case where narrowing cannot reach it — a tag named after an inherited member, where the own-property conjunction breaks it — and a single-tag variant is the same situation for a different reason.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The change is narrowly scoped to a TS-only type assertion in an unreachable arm and is backed by a dedicated new snapshot fixture.

Review effort: Lite
Findings: None

What changed in this PR

This PR improves the TypeScript-native bindings generator so that conversions for single-tag variants are type-safe under TypeScript’s control-flow narrowing rules, eliminating an unreachable fallthrough that previously retained the input type.

Changes:

  • Update the TS-native variant-to-candid conversion generator to emit value as never as the final fallthrough when the variant has exactly one tag.
  • Add a focused single_tag_variant test asset plus generated snapshots to lock in the expected output.
  • Register the new fixture in the generator snapshot test suite.
File Description
tests/​snapshots/​generate/​single_tag_variant/​single_tag_variant.ts.snapshot New snapshot covering the generated TS wrapper and conversions for a single-tag variant case.
tests/​snapshots/​generate/​single_tag_variant/​declarations/​single_tag_variant.did.js.snapshot New snapshot for generated candid JS declarations for the fixture.
tests/​snapshots/​generate/​single_tag_variant/​declarations/​single_tag_variant.did.d.ts.snapshot New snapshot for generated TS type declarations for the fixture.
tests/​generate.test.ts Includes single_tag_variant in the list of snapshot fixtures to run.
tests/​assets/​single_tag_variant.did Adds the new candid fixture exercising the single-tag variant behavior.
src/​core/​generate/​rs/​src/​bindings/​typescript_native/​conversion_functions_generator.rs Implements the as never fallthrough for single-tag __kind__-checked variant conversions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@marc0olo
marc0olo force-pushed the fix/single-tag-variant branch from 808e0ad to 7201f90 Compare September 21, 2026 15:17
@marc0olo

Copy link
Copy Markdown
Contributor Author

Force-pushed as part of a stack-wide rebase. This PR's own diff is unchanged — nothing to re-review here.

Only #175, #187 and #188 changed content in this round: a nested option reached through a type alias took the single-option path, so opt Inner with type Inner = opt Cfg was declared and converted unlike the opt opt Cfg it is. #187 fixes it for standalone types and record fields, #188 for variant payloads, and #175 corrects a doc comment that promised more than it did.

Comparing this PR's commit patch before and after the rebase, every added and removed line is identical. The only difference is one line of surrounding context in the fixture list — 'collide_class' where it now reads 'option_aliases' — because #187 registers a fixture in that list two positions above.

…ersion

A variant conversion tests each tag in turn and returns the input from the arm that no
tag matched. With two or more tags TypeScript has narrowed the input to `never` by
then, so the arm typechecks against any return type. A variant with one tag is not a
union, so nothing narrows: the arm keeps the input type, and the conversion of
`variant { p : opt nat }` fails to typecheck where the header is stripped, on the arm a
well-formed value never reaches.

The unreachable arm already asserts `never` where narrowing cannot reach it — a tag
named after an inherited member — and now does so for a single-tag variant as well.
The arm is erased at runtime and was unreachable before, so nothing on the wire moves.
@marc0olo
marc0olo force-pushed the fix/single-tag-variant branch from 7201f90 to 17916d4 Compare September 21, 2026 16:01
@marc0olo
marc0olo marked this pull request as ready for review September 21, 2026 17:33
@marc0olo
marc0olo requested a review from a team as a code owner September 21, 2026 17:33
@marc0olo

Copy link
Copy Markdown
Contributor Author

Dropped: we're fixing only what realistic interfaces hit silently. This one either fails loudly with a rename as the workaround, or needs names that don't occur in practice. The error only shows with @ts-nocheck removed, and runtime behaviour is unaffected.

@marc0olo marc0olo closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants