Skip to content

Expand discriminated unions in action templates - #2788

Draft
Dominic Nguyen (datduyng) wants to merge 3 commits into
mainfrom
domnguyen/actionTemplate-union-arms
Draft

Expand discriminated unions in action templates#2788
Dominic Nguyen (datduyng) wants to merge 3 commits into
mainfrom
domnguyen/actionTemplate-union-arms

Conversation

@datduyng

@datduyngDominic Nguyen (datduyng) commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Before (broken)

toTemplateType always collapsed every type-union to the first arm:

functiontoTemplateType(type: ActionParamType,visited: ReadonlySet<string>=newSet<string>(),): TemplateType|undefined{switch(type.type){case"type-union":
// TODO: smarter about type unions.returntoTemplateType(type.types[0],visited);// ← always arm 0// ...}}// parameters built with no current values:consttype=toTemplateType(actionParametersType);

For player MusicTarget, that means the form template only ever sees the track arm:

// playerSchema.ts — 7 authored armsexporttypeMusicTarget=|PlayByTrack// kind: "track"|PlayByArtist// kind: "artist"|PlayByAlbum// kind: "album"|PlayByGenre// kind: "genre"|PlayByPlaylist// kind: "playlist"|PlayByDescription// kind: "description"|PlayAnyMusic;// kind: "any"exportinterfacePlayByTrack{kind: "track";trackName: string;artists?: string[];albumName?: string;}exportinterfacePlayByArtist{kind: "artist";artist: string;genre?: string;}exportinterfacePlayByAlbum{kind: "album";albumName: string;artists?: string[];}// ... genre / playlist / description / any
// template produced BEFORE (union → types[0] only){type: "object",fields: {kind: {type: {type: "string-union",typeEnum: ["track"]}},// ← only tracktrackName: {type: {type: "string"}},artists: {optional: true,type: {type: "array",elementType: {type: "string"}}},albumName: {optional: true,type: {type: "string"}},}}// lost: artist | album | genre | playlist | description | any
kind enum visible in template: [ 'track' ]
template only exposes first kind: [ 'track' ]
lost arms: [ 'artist', 'album', 'genre', 'playlist', 'description', 'any' ]
RESULT: BUG REPRODUCED

After (fixed)

Pass current parameter data into template conversion, detect MusicTarget-style discriminated object unions, emit the fullkind enum, and select arm fields from data.kind:

// Wire current values so the selected arm can be resolvedconsttype=toTemplateType(actionParametersType,newSet(),action.parameters,// ← was missing);
functiontoTemplateType(type: ActionParamType,visited: ReadonlySet<string>=newSet<string>(),data: unknown=undefined,): TemplateType|undefined{switch(type.type){case"type-union":
returntoTemplateTypeUnion(type.types,visited,data);case"type-reference":
// ... resolve, pass data through ...returntoTemplateType(type.definition.type,nextVisited,data);case"object":
returntoTemplateTypeObject(type,visited,data);case"array":
returntoTemplateTypeArray(type,visited,data);// ...}}
functiontoTemplateTypeUnion(types: readonlyActionParamType[],visited: ReadonlySet<string>,data: unknown,): TemplateType|undefined{constdisc=getObjectUnionDiscriminator(types);if(disc!==undefined){const{ fieldName, values, arms }=disc;letselectedIndex=0;if(data!==null&&typeofdata==="object"&&!Array.isArray(data)&&fieldNamein(dataasobject)){constcurrent=(dataasRecord<string,unknown>)[fieldName];if(typeofcurrent==="string"){constidx=values.indexOf(current);if(idx>=0)selectedIndex=idx;}}consttemplate=toTemplateTypeObject(arms[selectedIndex],visited,data);// Full enum so the UI can switch arms; discriminator triggers schema refresh.template.fields[fieldName]={optional: false,type: {type: "string-union",typeEnum: values,// all 7 kindsdiscriminator: values[selectedIndex],},};returntemplate;}// Non-discriminated: first-arm fallback (historical behavior)returntoTemplateType(types[0],visited,data);}
// Discriminator discovery: every arm is an object with the same single-value// string-union field (e.g. kind: "track" | kind: "artist" | …)functiongetObjectUnionDiscriminator(types: readonlyActionParamType[],): {fieldName: string;values: string[];arms: ActionParamObject[]}|undefined{if(types.length<2)returnundefined;constarms: ActionParamObject[]=[];for(consttoftypes){constobj=resolveObjectType(t);if(obj===undefined)returnundefined;arms.push(obj);}for(constfieldNameofObject.keys(arms[0].fields)){constvalues: string[]=[];letok=true;for(constarmofarms){constfield=arm.fields[fieldName];if(field===undefined||field.optional){ok=false;break;}constft=resolveTypeReference(field.type)??field.type;if(ft.type!=="string-union"||ft.typeEnum.length!==1){ok=false;break;}values.push(ft.typeEnum[0]);}if(!ok)continue;if(newSet(values).size!==values.length)continue;// must uniquely selectreturn{ fieldName, values, arms };}returnundefined;}
// template produced AFTER — data.kind = "album"{type: "object",fields: {kind: {type: {type: "string-union",typeEnum: ["track","artist","album","genre","playlist","description","any",],discriminator: "album",},},albumName: {type: {type: "string"}},artists: {optional: true,type: {type: "array",elementType: {type: "string"}}},}}
// AFTER — data.kind = "track"
fields: ["kind","trackName","artists","albumName"]
kind enum: [track,artist,album,genre,playlist,description,any]
discriminator: "track"// AFTER — data.kind = "artist"
fields: ["kind","artist","genre"]
discriminator: "artist"// AFTER — data.kind = "album"
fields: ["kind","albumName","artists"]
discriminator: "album"
RESULT: FIX VERIFIED
full kind enum on template: [track, artist, album, genre, playlist, description, any]

- type-union previously always took types[0] (MusicTarget kind=[track] only)
- Detect MusicTarget-style object unions via single-value kind discriminators
- Emit full kind string-union + arm fields selected from current data.kind
- Enables form/edit UI to switch arms via getTemplateSchema refresh
- Tests for full kind enum, album/track arm selection, non-union unchanged
@datduyng
Dominic Nguyen (datduyng) marked this pull request as draft August 3, 2026 17:08
- Drop long JSDoc blocks on union helpers
- Leave short line comments only
Sign up for freeto 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.

1 participant

@datduyng