Skip to content

[emitter-framework] Render discriminated unions correctly #7369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions .chronus/changes/discriminated-union-ef-2025-4-15-22-11-29.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/emitter-framework"
---

Render discriminated unions correctly
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function InterfaceBody(props: TypedInterfaceDeclarationProps): Children {

return (
<>
<ay.For each={validTypeMembers} line {...enderProp}>
<ay.For each={validTypeMembers} semicolon line {...enderProp}>
{(typeMember) => {
return <InterfaceMember type={typeMember} />;
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as ay from "@alloy-js/core";
import { Children } from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";
import { Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler";
import { compilerAssert, Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler";
import { useTsp } from "../../core/context/tsp-context.js";
import { efRefkey } from "../utils/refkey.js";
import { TypeExpression } from "./type-expression.jsx";

export interface UnionExpressionProps {
Expand All @@ -19,11 +20,30 @@ export function UnionExpression({ type, children }: UnionExpressionProps) {

const variants = (
<ay.For joiner={" | "} each={items}>
{(_, value) => {
if ($.enumMember.is(value)) {
return <ts.ValueExpression jsValue={value.value ?? value.name} />;
} else {
return <TypeExpression type={value.type} />;
{(_, type) => {
if ($.enumMember.is(type)) {
return <ts.ValueExpression jsValue={type.value ?? type.name} />;
}

const discriminatedUnion = $.union.getDiscriminatedUnion(type.union);
switch (discriminatedUnion?.options.envelope) {
case "object":
return (
<ObjectEnvelope
discriminatorPropertyName={discriminatedUnion.options.discriminatorPropertyName}
envelopePropertyName={discriminatedUnion.options.envelopePropertyName}
type={type}
/>
);
case "none":
return (
<NoneEnvelope
discriminatorPropertyName={discriminatedUnion.options.discriminatorPropertyName}
type={type}
/>
);
default:
return <TypeExpression type={type.type} />;
}
}}
</ay.For>
Expand All @@ -32,10 +52,83 @@ export function UnionExpression({ type, children }: UnionExpressionProps) {
if (children || (Array.isArray(children) && children.length)) {
return (
<>
{variants} {` | ${children}`}
{variants} {`| ${children}`}
</>
);
}

return variants;
}

interface ObjectEnvelopeProps {
type: UnionVariant;
discriminatorPropertyName: string;
envelopePropertyName: string;
}

/**
* Renders a discriminated union with "object" envelope style
* where model properties are nested inside an envelope
*/
function ObjectEnvelope(props: ObjectEnvelopeProps) {
const { $ } = useTsp();

const envelope = $.model.create({
properties: {
[props.discriminatorPropertyName]: $.modelProperty.create({
name: props.discriminatorPropertyName,
type: $.literal.createString(props.type.name as string),
}),
[props.envelopePropertyName]: $.modelProperty.create({
name: props.envelopePropertyName,
type: props.type.type,
}),
},
});

return <TypeExpression type={envelope} />;
}

interface NoneEnvelopeProps {
type: UnionVariant;
discriminatorPropertyName: string;
}

/**
* Renders a discriminated union with "none" envelope style
* where discriminator property sits alongside model properties
*/
function NoneEnvelope(props: NoneEnvelopeProps) {
const { $ } = useTsp();

compilerAssert(
$.model.is(props.type.type),
"Expected all union variants to be models when using a discriminated union with no envelope",
);

// Render anonymous models as a set of properties + the discriminator
if ($.model.isExpresion(props.type.type)) {
const model = $.model.create({
properties: {
[props.discriminatorPropertyName]: $.modelProperty.create({
name: props.discriminatorPropertyName,
type: $.literal.createString(props.type.name as string),
}),
...Object.fromEntries(props.type.type.properties),
},
});
return <TypeExpression type={model} />;
}

return (
<ay.List joiner={" & "}>
<ts.ObjectExpression>
<ts.ObjectProperty
name={props.discriminatorPropertyName}
value={<ts.ValueExpression jsValue={props.type.name} />}
/>
</ts.ObjectExpression>
<>{efRefkey(props.type.type)}</>
</ay.List>
);
}
Loading
Loading