Skip to content

fix(#19, #20): support complex unions and literal unions#25

Merged
mvhenten merged 1 commit intomainfrom
fix-complex-unions
Dec 3, 2025
Merged

fix(#19, #20): support complex unions and literal unions#25
mvhenten merged 1 commit intomainfrom
fix-complex-unions

Conversation

@mvhenten
Copy link
Owner

@mvhenten mvhenten commented Dec 3, 2025

No description provided.

Copilot AI review requested due to automatic review settings December 3, 2025 14:43
@mvhenten mvhenten merged commit b707b81 into main Dec 3, 2025
7 checks passed
@mvhenten mvhenten deleted the fix-complex-unions branch December 3, 2025 14:44
@github-actions
Copy link

github-actions bot commented Dec 3, 2025

🎉 This PR is included in version 3.0.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for both literal unions (e.g., "home" | "work" | "other") and complex unions (e.g., BooleanValue | Int64Value) to address issues #19 and #20. Literal unions are emitted as enum-like arrays, while complex unions use ElectroDB's CustomAttributeType with a TypeScript type annotation.

  • Literal unions are now emitted as arrays of string values (like enums)
  • Complex unions are emitted using CustomAttributeType with proper TypeScript type annotations
  • Added RawCode marker class to support emitting raw code expressions during code generation

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
test/main.tsp Adds test models for complex unions (Int64Value, BooleanValue, Info) and adds additionalInfo field to Person model to test union handling
test/entities.test.js Updates test description for literal union behavior and adds comprehensive test suite for complex union types with CustomAttributeType
src/stringify.ts Introduces RawCode class for emitting raw code expressions and enables TypeScript parser plugin for handling generic syntax
src/emitter.ts Adds emitTypeToTypeScript for TypeScript type generation, isLiteralUnion to detect simple literal unions, emitUnion to handle both union types, and conditional import of CustomAttributeType

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +113 to +173
function emitTypeToTypeScript(type: Type): string {
switch (type.kind) {
case "Scalar": {
let baseType = type;
while (baseType.baseScalar) {
baseType = baseType.baseScalar;
}
switch (baseType.name) {
case "boolean":
return "boolean";
case "numeric":
case "integer":
case "float":
case "int64":
case "int32":
case "int16":
case "int8":
case "uint64":
case "uint32":
case "uint16":
case "uint8":
case "safeint":
case "float32":
case "float64":
case "decimal":
case "decimal128":
return "number";
default:
return "string";
}
}
case "Model": {
if (type.name === "Array") {
const arrayType = type as ArrayModelType;
return `${emitTypeToTypeScript(arrayType.indexer.value)}[]`;
}
const properties: string[] = [];
for (const prop of walkPropertiesInherited(type as RecordModelType)) {
const optional = prop.optional ? "?" : "";
properties.push(
`${prop.name}${optional}: ${emitTypeToTypeScript(prop.type)}`,
);
}
return `{ ${properties.join("; ")} }`;
}
case "Enum": {
const values = Array.from(type.members)
.map(([key, member]) => `"${member.value ?? key}"`)
.join(" | ");
return values;
}
case "Union": {
const variants = Array.from(type.variants.values())
.map((variant) => emitTypeToTypeScript(variant.type))
.join(" | ");
return variants;
}
default:
return "any";
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emitTypeToTypeScript function duplicates scalar type mapping logic that already exists in emitIntrinsincScalar (lines 24-50). Consider refactoring to reuse the existing function to improve maintainability and reduce the risk of inconsistencies.

For example, the Scalar case could be simplified to:

case "Scalar": {
    let baseType = type;
    while (baseType.baseScalar) {
        baseType = baseType.baseScalar;
    }
    return emitIntrinsincScalar(baseType);
}

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +210
function emitTypeToTypeScript(type: Type): string {
switch (type.kind) {
case "Scalar": {
let baseType = type;
while (baseType.baseScalar) {
baseType = baseType.baseScalar;
}
switch (baseType.name) {
case "boolean":
return "boolean";
case "numeric":
case "integer":
case "float":
case "int64":
case "int32":
case "int16":
case "int8":
case "uint64":
case "uint32":
case "uint16":
case "uint8":
case "safeint":
case "float32":
case "float64":
case "decimal":
case "decimal128":
return "number";
default:
return "string";
}
}
case "Model": {
if (type.name === "Array") {
const arrayType = type as ArrayModelType;
return `${emitTypeToTypeScript(arrayType.indexer.value)}[]`;
}
const properties: string[] = [];
for (const prop of walkPropertiesInherited(type as RecordModelType)) {
const optional = prop.optional ? "?" : "";
properties.push(
`${prop.name}${optional}: ${emitTypeToTypeScript(prop.type)}`,
);
}
return `{ ${properties.join("; ")} }`;
}
case "Enum": {
const values = Array.from(type.members)
.map(([key, member]) => `"${member.value ?? key}"`)
.join(" | ");
return values;
}
case "Union": {
const variants = Array.from(type.variants.values())
.map((variant) => emitTypeToTypeScript(variant.type))
.join(" | ");
return variants;
}
default:
return "any";
}
}

function isLiteralUnion(type: Union): string[] | null {
const literals: string[] = [];

for (const variant of type.variants.values()) {
// Check if this variant is a string or number literal
if (variant.type.kind === "String") {
literals.push(variant.type.value);
} else if (variant.type.kind === "Number") {
literals.push(String(variant.type.value));
} else {
// Not a literal union, return null
return null;
}
}

return literals;
}

function emitUnion(type: Union): Attribute {
// Check if this is a simple literal union (e.g., "home" | "work" | "other")
const literals = isLiteralUnion(type);
if (literals) {
// Emit as enum-like array, similar to how named enums are handled
return {
type: literals,
};
}

// Complex union - use CustomAttributeType
const tsType = emitTypeToTypeScript(type);
// RawCode is used to emit the CustomAttributeType function call as-is
return {
// @ts-expect-error - RawCode is handled by stringifyObject at code generation time
type: new RawCode(`CustomAttributeType<${tsType}>("any")`),
};
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new functions (emitTypeToTypeScript, isLiteralUnion, and emitUnion) lack documentation explaining their purpose, parameters, and return values. Adding JSDoc comments would improve code maintainability and help other developers understand the code's intent.

For example:

/**
 * Converts a TypeSpec type to its TypeScript type representation as a string.
 * @param type - The TypeSpec type to convert
 * @returns A string representing the TypeScript type
 */
function emitTypeToTypeScript(type: Type): string {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants