An inline anonymous variant takes its TypeScript type from whichever named candid type declared the same tags first. Interning follows env.to_sorted_iter(), so "first" means alphabetically first — and a method's published type therefore depends on the names of unrelated candid types.
Reproduction
type RepairStatus = variant { delivered; inProgress; ready; received };
type SoftwareServiceStatus = variant { delivered; inProgress; ready; received };
service : () -> {
peek : () -> (variant { delivered; inProgress; ready; received }) query;
}
peek(): Promise<RepairStatus>;
peek returns RepairStatus only because R sorts before S. Adding an unrelated type AaaStatus with the same tags silently changes peek's return type, breaking callers with no diagnostic. The .did edit that causes it is semantically irrelevant.
The decision
Should an inline anonymous variant's type be derived from its own tags, or borrowed from a named type that happens to match?
Borrowing is what creates the coupling; deriving is what makes it stable. Everything below follows from that one choice.
Options
|
peek returns |
stable |
interop with a same-valued named enum |
A — its own Variant_<tags> enum |
Variant_delivered_inProgress_ready_received |
yes |
neither direction |
| B — a string-literal union |
"delivered" | "inProgress" | "ready" | "received" |
yes |
both directions |
| C — keep borrowing, document it |
RepairStatus |
no |
full |
A is the obvious fix and the weakest. TypeScript enums are nominal, so it trades an unstable-but-usable type for a stable-but-isolated one — const s: RepairStatus = await actor.peek() compiles today and would not afterwards:
error TS2322: Type 'Variant_delivered_ready' is not assignable to type 'RepairStatus'.
It also adds a declaration whose members duplicate an existing enum.
B derives the type from the tags, so it is stable, and a string enum member is assignable to its literal type and back — RepairStatus.ready still flows in and out. It removes a declaration rather than adding one. The cost is that the type stops being a named thing consumers can import; they get a structural union.
C is defensible on frequency: the hazard needs an inline anonymous variant, a named type with identical tags, and someone later adding another such type.
Notes
Pre-existing — the previous HashMap-keyed interning had the same insertion-order dependence. #173 restructures this code and keeps the behaviour deliberately, so it neither fixes nor worsens it.
Whichever of A or B is chosen is a change to a published type for every anonymous variant, which is a wider blast radius than an ordinary bug fix and belongs in its own PR.
An inline anonymous variant takes its TypeScript type from whichever named candid type declared the same tags first. Interning follows
env.to_sorted_iter(), so "first" means alphabetically first — and a method's published type therefore depends on the names of unrelated candid types.Reproduction
peekreturnsRepairStatusonly becauseRsorts beforeS. Adding an unrelatedtype AaaStatuswith the same tags silently changespeek's return type, breaking callers with no diagnostic. The.didedit that causes it is semantically irrelevant.The decision
Should an inline anonymous variant's type be derived from its own tags, or borrowed from a named type that happens to match?
Borrowing is what creates the coupling; deriving is what makes it stable. Everything below follows from that one choice.
Options
peekreturnsVariant_<tags>enumVariant_delivered_inProgress_ready_received"delivered" | "inProgress" | "ready" | "received"RepairStatusA is the obvious fix and the weakest. TypeScript enums are nominal, so it trades an unstable-but-usable type for a stable-but-isolated one —
const s: RepairStatus = await actor.peek()compiles today and would not afterwards:It also adds a declaration whose members duplicate an existing enum.
B derives the type from the tags, so it is stable, and a string enum member is assignable to its literal type and back —
RepairStatus.readystill flows in and out. It removes a declaration rather than adding one. The cost is that the type stops being a named thing consumers can import; they get a structural union.C is defensible on frequency: the hazard needs an inline anonymous variant, a named type with identical tags, and someone later adding another such type.
Notes
Pre-existing — the previous
HashMap-keyed interning had the same insertion-order dependence. #173 restructures this code and keeps the behaviour deliberately, so it neither fixes nor worsens it.Whichever of A or B is chosen is a change to a published type for every anonymous variant, which is a wider blast radius than an ordinary bug fix and belongs in its own PR.