VIP-16: data declarations (tagged unions)
Status: accepted by Will, 2026-10-28.
Decision
// VibeLang
data Delivery { Waiting; Sent { tracking: string }; Rejected { reason: string } }
const d = Delivery.Sent({ tracking: "1Z" })
match (d) { when { _tag: "Sent", let tracking }: tracking; default: "none"; }
// exactly this Effect TS
type Delivery = Data.TaggedEnum<{ Waiting: {}; Sent: { tracking: string }; Rejected: { reason: string } }>
const Delivery = Data.taggedEnum<Delivery>()-
data Name { Variant; Variant { fields } }declares Effect'sData.TaggedEnumtype and itsData.taggedEnumconstructors under one name. -
Variant names
$isand$matchareEK1602, including in generic data: those properties are Effect's helpers. Rename the variant and its resolved references; other variants retain the exactData.taggedEnumconstructors. -
Variants are matched with TC39 object patterns on
_tag(VIP-7). In the pinned TC39 snapshot, a bare key such as{ tracking }only tests that the property exists; binding it takeslet tracking, the snapshot's shorthand fortracking: let tracking. The variant constructors get no custom matchers. -
Generic unions (
data Result<A, E> { … }) are left to a later VIP. -
Ambiguity: none.
datais a contextual keyword on the same line as its name, soconst data = …still works. -
Amended 2026-10-29: zero variants.
data Empty {}is allowed: an exact emptyData.TaggedEnum(the old EK2122 is deleted). Sent to Will on Telegram 2026-10-29 12:28 under his standing rule 'assume I accept'; no objection by 13:00. -
Amended 2026-10-30: payload key admission and erasure. Payload fields use the ordinary TypeScript type-member computed-name checks. Duplicate members retain native TypeScript checking and merging. A broad entity-name key can form an index signature, while a dynamic expression can still be
TS1170; aPropertyKeyresult alone is not an admission proof. These payload types are erased, so their key expressions add no runtime lookup, call or TDZ read when data is declared or constructed. Separate value declarations retain their normal evaluation. The generatedData.TaggedEnumconstraint still rejects a payload that conflicts with_tag. -
Amended 2026-10-30: named default exports. A direct
export default data D { … }isEK1603: declareDand writeexport { D as default }. This catalogs the existing named-declaration grammar and changes neither generic data support nor the variant payload/helper diagnostics. -
Amended 2026-10-30: incomplete declaration diagnostics. A recognized data declaration missing its required variant body is
EK1604. An explicitly written empty body remains admitted; recovery does not supply one automatically. These codes label existing rejections; they add no admitted syntax. -
Amended 2026-10-31: malformed variants. A malformed
datavariant or a payload member that is not aFieldisEK1606.EK1601toEK1605keep their specific triggers. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 247).
History
One lego finding (D405): an exhaustive match over a data value skipped its last arm's test. VIP-7 settles it, because the exhaustiveness check never changes what runs.
Prior art
Rust and Swift enums with payloads, Kotlin sealed classes, Effect's Data.TaggedEnum.
Will's words
"OK I accpet with your recomendations"
(The recommendations: match with object patterns, and decide generic unions in a later VIP.)
Corrected 2026-10-29: the example now binds with let, using TC39's let tracking shorthand (found by the differential fuzz suite).
Corrected 2026-10-29: the example ends every match arm with ;, the last one included, as the pinned TC39 grammar requires; a newline is not a separator (found by the round-3 review).