Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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