Skip to content

fix(typescript): scope union aliases to same-namespace with >1 subclass#227

Open
muhabdulkadir wants to merge 6 commits into
accordproject:mainfrom
muhabdulkadir:moh/fix-unions
Open

fix(typescript): scope union aliases to same-namespace with >1 subclass#227
muhabdulkadir wants to merge 6 commits into
accordproject:mainfrom
muhabdulkadir:moh/fix-unions

Conversation

@muhabdulkadir
Copy link
Copy Markdown
Contributor

@muhabdulkadir muhabdulkadir commented May 20, 2026

Closes #

Union aliases were generated for single-subclass cases (producing useless singleton unions like type XUnion = IChild) and pulled cross-namespace subclasses into the parent namespace, creating inverted dependencies, potential name collisions, and unbounded unions on system types like Concept.

PS: Cross-namespace unions are a consumer-side concern; they compose the union from the specific subtypes they need.

Changes

  • Union alias emission now requires >1 non-enum direct subclass in the same namespace before generating export type XUnion = ...
  • flattenSubclassesToUnion field behavior applies the same >1 same-namespace threshold
  • Cross-namespace subclass imports removed, parent namespaces no longer import type from child namespaces solely to define unions

Before

Example 1: Duplicate members in union

concerto@1.0.0 generates a ConceptUnion that includes cross-namespace subclasses. The result has ICategory multiple times (one from each namespace that extends Concept):

// Before — concerto@1.0.0.ts
export type ConceptUnion = ICategory | 
IAddress | 
ICategory |   // ← duplicate
IInfo | 
ICompany;

This union is unusable: TypeScript deduplicates the duplicate ICategory, but the intent is ambiguous (which ICategory?), and none of these types are defined in this file's namespace.

Example 2: Singleton unions are noise

Every parent with exactly one subclass gets a union that's just an alias for one type:

// Before
export type AssetUnion = IEquipment;
export type ParticipantUnion = IPerson;
export type EmployeeUnion = IManager;
export type CompanyEventUnion = IOnboarded;
export type CategoryUnion = IGeneralCategory;

A union of one type adds no value; AssetUnion is just IEquipment. Consumers can use IEquipment directly.

Example 3: Cross-namespace imports only existed to feed these unions

To build the broken unions, we imported every subclass from every namespace:

// Before — concerto@1.0.0.ts
// Warning: Beware of circular dependencies when modifying these imports
import type { IEquipment } from './org.acme.hr@1.0.0';
// Warning: Beware of circular dependencies when modifying these imports
import type { IPerson } from './org.acme.hr@1.0.0';
// Warning: Beware of circular dependencies when modifying these imports
import type { IChangeOfAddress } from './org.acme.hr@1.0.0';
// Warning: Beware of circular dependencies when modifying these imports
import type { ICompanyEvent } from './org.acme.hr@1.0.0';

Separate import type blocks from the same namespace (not even consolidated), each with a circular-dependency warning. These imports were only needed for the singleton unions above; removing the broken unions removes the need for these imports entirely.

After

Example 1: No broken union

// After — concerto@1.0.0.ts
export interface IConcept {
   $class: string;
}
// No ConceptUnion — subclasses span multiple namespaces

Example 2: No singleton unions

// After
export interface IAsset extends IConcept {
   $identifier: string;
}
// No AssetUnion — only one subclass (IEquipment)

export interface IParticipant extends IConcept {
   $identifier: string;
}
// No ParticipantUnion — only one subclass (IPerson)

export interface IEmployee extends IPerson {
   manager?: IManager;
}
// No EmployeeUnion — only one subclass (IManager)

Example 3: No cross-namespace import noise

// After — concerto@1.0.0.ts
// imports
// (no cross-namespace subclass imports — no warnings, no circular deps)

// interfaces
export interface IConcept {
   $class: string;
}

export interface IAsset extends IConcept {
   $identifier: string;
}

export interface IParticipant extends IConcept {
   $identifier: string;
}

export interface ITransaction extends IConcept {
   $timestamp: Date;
}

export interface IEvent extends IConcept {
   $timestamp: Date;
}

Related Issues

  • Issue #
  • Pull Request #

Author Checklist

  • Ensure you provide a DCO sign-off for your commits using the --signoff option of git commit.
  • Vital features and changes captured in unit and/or integration tests
  • Commits messages follow AP format
  • Extend the documentation, if necessary
  • Merging to main from fork:branchname

…ve scalar names in fields and maps

Signed-off-by: muhammed-abdulkadir <muhammed.abdulkadir@docusign.com>
Signed-off-by: muhammed-abdulkadir <muhammed.abdulkadir@docusign.com>
@muhabdulkadir
Copy link
Copy Markdown
Contributor Author

this pr is a follow-up for #226

@muhabdulkadir muhabdulkadir changed the title moh/fix unions fix(typescript): scope union aliases to same-namespace with >1 subclass May 20, 2026
muhabdulkadir and others added 2 commits May 20, 2026 14:27
Signed-off-by: Muhammed Abdulkadir <40035796+muhabdulkadir@users.noreply.github.com>
Signed-off-by: muhammed-abdulkadir <muhammed.abdulkadir@docusign.com>
Copy link
Copy Markdown
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 updates the TypeScript code generator to stop emitting “singleton” union aliases and to scope generated union aliases/import behavior to subclasses within the same namespace, avoiding cross-namespace dependency inversion and import noise.

Changes:

  • Union alias emission now requires >1 non-enum direct subclass in the same namespace.
  • flattenSubclassesToUnion now applies the same “>1 same-namespace subclass” threshold before using XUnion types.
  • Removes cross-namespace subclass imports that previously existed solely to build cross-namespace unions.

Reviewed changes

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

File Description
test/codegen/fromcto/typescript/typescriptvisitor.js Updates/extends unit tests to reflect same-namespace-only union rules and removal of cross-namespace subclass imports.
test/codegen/snapshots/codegen.js.snap Updates generated output snapshots to remove singleton unions and cross-namespace subclass import blocks.
lib/codegen/fromcto/typescript/typescriptvisitor.js Implements same-namespace + “>1 subclass” threshold for union alias generation and for flattenSubclassesToUnion; removes cross-namespace subclass import generation.

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

Comment thread lib/codegen/fromcto/typescript/typescriptvisitor.js Outdated
Comment on lines +294 to +297
if (sameNsSubclasses.length > 1) {
const useUnion = !(isEnumRef || isMapRef);
tsType = this.toTsType(field.getType(), !useUnion, useUnion);
}
muhabdulkadir and others added 2 commits May 20, 2026 14:47
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Muhammed Abdulkadir <40035796+muhabdulkadir@users.noreply.github.com>
Signed-off-by: muhammed-abdulkadir <muhammed.abdulkadir@docusign.com>
Sign up for free to 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.

3 participants