Skip to content

feat(compiler): add extends base type clause for unions - #11771

Merged
Timothee Guerin (timotheeguerin) merged 8 commits into
microsoft:mainfrom
JoshLove-msft:josh/union-extends
Sep 3, 2026
Merged

feat(compiler): add extends base type clause for unions#11771
Timothee Guerin (timotheeguerin) merged 8 commits into
microsoft:mainfrom
JoshLove-msft:josh/union-extends

Conversation

@JoshLove-msft

@JoshLove-msft JoshLove-msft commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #2737

Adds experimental extends support to named union declarations.

Enable the feature in tspconfig.yaml to use it without an experimental warning:

features:
  - union-extends
model PetBase {
  name: string;
}
model Cat extends PetBase {
  toy: string;
}
model Dog extends PetBase {
  food: string;
}

union Pet extends PetBase {
  cat: Cat,
  dog: Dog,
}

Semantics

extends on a union is purely an assignability constraint:

  • Every variant must be assignable to the base type; a diagnostic is reported on any variant that is not.
  • The resolved base type is exposed on the type graph as Union.baseType, giving emitters an easy way to represent the union with a polymorphic base type in languages that do not support unions natively.
  • The base type does not become a variant of the union.
  • It does not create an inheritance relationship, make the union extensible, or interact with @discriminator.
  • Deprecation is deliberately not copied from the base type onto the union, unlike scalar extends.

The base expression must resolve to a model, scalar, enum, or union. Union, intersection, array, and template expressions are supported when they resolve to one of those data types. Anonymous model expressions are rejected, including through aliases.

Per #2737 (comment), extends remains the constraint keyword because template constraints already use it for structural assignability. Nominal typing and extensible unions remain separate concerns in #3900 and #3901.

Implementation

Area Change
Feature gate Adds the union-extends compiler feature and reports experimental-feature when it is not enabled
AST Adds UnionStatementNode.extends and narrows Union.baseType to Model | Scalar | Enum | Union
Parser/checker Parses the optional clause, resolves its data type, validates each variant, and detects circular base-type references
Type graph Navigates and mutates Union.baseType
Formatter Prints the heritage clause and uses shared empty-declaration comment handling
Tooling Updates TextMate grammar, LSP completion, compiler feature completion, the language spec, and docs

Circular references

union a extends a and alias indirection are caught by the existing pendingResolutions/ResolutionKind.BaseType mechanism.

Because a union constraint can also be a union expression, references such as union a extends a | string are detected on the resolved type. That walk deliberately follows only union expressions, so legal cyclic data graphs remain accepted:

model Box {
  inner: a;
}
union a extends Box {
  x: Box,
}

Templates

Variant assignability is skipped inside an uninstantiated template declaration. Each instantiation is checked, and each valid instantiation gets its own concrete baseType.

Validation

  • Full @typespec/compiler suite: 4,196 passed, 6 skipped
  • TypeScript build/typecheck: clean
  • Compiler lint and Prettier checks: clean

A named union can now declare a base type with `extends`. Every variant
must be assignable to that base type, and the resolved type is exposed on
the type graph as `Union.baseType` so emitters can represent the union
with a polymorphic base type in languages without native unions.

`extends` on a union is purely a constraint: it doesn't create any
inheritance relationship, the base type doesn't become a variant, it
doesn't make the union extensible and it has no interaction with
`@discriminator`.

Fixes microsoft#2737

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be5c2e95-cfc7-417c-bc70-b34cf66bbea4
@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@11771

commit: ad3f39c

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/compiler
Show changes

@typespec/compiler - feature ✏️

Add experimental support for an extends clause on union statements to constrain every variant to a common data type.,> ,> Enable the union-extends compiler feature in tspconfig.yaml to use the clause.,> ,> tsp,> model PetBase {,> name: string;,> },> model Cat extends PetBase {,> toy: string;,> },> model Dog extends PetBase {,> food: string;,> },> ,> union Pet extends PetBase {,> cat: Cat,,> dog: Dog,,> },> ,> ,> The base type is exposed on the type graph as Union.baseType, giving emitters an easy way to know that all the variants of a union share a common base type. A diagnostic is reported on any variant that isn't assignable to the base type.,> ,> extends on a union is purely a constraint: it doesn't imply any subtyping relationship, it doesn't make the union extensible and it has no interaction with @discriminator.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds extends support to named union declarations as an assignability constraint (exposed as Union.baseType), enabling better authoring validation and simpler emitter modeling of unions via a polymorphic base type.

Changes:

  • Compiler: parse/check union <Name> extends <Expression> { ... }, set Union.baseType, validate each variant is assignable, and detect circular base-type references.
  • Tooling/UX: update LSP completion, TextMate grammars/colorization, semantic-walker navigation, and experimental mutator graph support.
  • Docs/tests: document the feature and add broad test coverage across parser/checker/formatter/tooling.

Reviewed changes

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

Show a summary per file
File Description
website/src/content/docs/docs/language-basics/unions.md Documents union extends as a constraint and clarifies semantics for users/emitters.
packages/spec/src/spec.emu.html Updates the published grammar to include an optional UnionExtends.
packages/compiler/test/server/completion.test.ts Adds keyword + identifier completion coverage for union extends.
packages/compiler/test/server/colorization.test.ts Adds tokenization tests for unions with extends (incl. templated unions).
packages/compiler/test/semantic-walker.test.ts Ensures semantic navigation traverses Union.baseType.
packages/compiler/test/parser.test.ts Extends parser roundtrip/error coverage for union extends syntax.
packages/compiler/test/formatter/scenarios/outputs/union.tsp Updates formatter scenario outputs for unions with extends + empty-body comment case.
packages/compiler/test/formatter/scenarios/inputs/union.tsp Adds formatter inputs for unions with extends and comment preservation.
packages/compiler/test/formatter/formatter.test.ts Adds unit tests validating formatting of union extends clauses and comment behavior.
packages/compiler/test/experimental/mutator.test.ts Verifies global graph mutation includes Union.baseType.
packages/compiler/test/checker/union.test.ts Adds comprehensive semantics tests for baseType setting, diagnostics, templates, cycles, and deprecation behavior.
packages/compiler/src/server/tmlanguage.ts Adds TextMate rules for union extends highlighting.
packages/compiler/src/server/completion.ts Enables extends keyword completion in union headers.
packages/compiler/src/formatter/print/printer.ts Prints union heritage clause and preserves dangling comments in empty unions.
packages/compiler/src/formatter/print/comment-handler.ts Adds comment handling to attach empty-union comments correctly.
packages/compiler/src/experimental/mutators.ts Mutates Union.baseType as part of union graph mutation.
packages/compiler/src/core/types.ts Adds Union.baseType and UnionStatementNode.extends to core type/AST definitions.
packages/compiler/src/core/semantic-walker.ts Navigates Union.baseType during semantic walking.
packages/compiler/src/core/parser.ts Parses optional union extends clause and includes it in AST traversal.
packages/compiler/src/core/checker.ts Checks/sets Union.baseType, validates variants against it, and handles circular-reference detection.
grammars/typespec.json Updates JSON TextMate grammar with union-extends.
.chronus/changes/union-extends-base-type-2026-8-26.md Adds changelog entry for the new compiler feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread grammars/typespec.json
Comment thread packages/compiler/src/server/tmlanguage.ts
@azure-sdk-automation

azure-sdk-automation Bot commented Aug 27, 2026

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

Comment thread packages/compiler/src/formatter/print/comment-handler.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.

Comment thread packages/compiler/src/core/types.ts Outdated
Comment thread packages/compiler/src/core/parser.ts
Comment thread packages/compiler/src/core/checker.ts
Comment thread packages/compiler/src/server/tmlanguage.ts
Gate union extends behind an experimental compiler feature, restrict base types to data declarations, reject model expressions, and consolidate empty-declaration comment handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 28, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.

Comment thread website/src/content/docs/docs/language-basics/unions.md Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4518d421-bd02-4083-abc6-c5bffa94918e
Copilot AI review requested due to automatic review settings August 28, 2026 21:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 30, 2026 03:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comment thread packages/compiler/src/core/features.ts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 3, 2026 00:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new Union.baseType JSDoc currently overstates assignability guarantees for uninstantiated template declarations, which could mislead emitter authors consuming the type graph.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

packages/compiler/src/core/types.ts:690

  • Union.baseType is documented as guaranteeing every variant is assignable, but variant assignability checks are explicitly skipped in CheckFlags.InTemplateDeclaration (see checkUnionVariantAgainstBaseType), while baseType can still be set on the uninstantiated template declaration (e.g. union Foo<T> extends string { value: T }). This makes the current JSDoc guarantee inaccurate for templated unions.
  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 3, 2026 02:14
@JoshLove-msft

Copy link
Copy Markdown
Contributor Author

Addressed the Copilot review in 150406b92d. The Union.baseType JSDoc now states that assignability violations are reported as diagnostics and that validation is deferred for uninstantiated template declarations until each template instance. I also added coverage showing that the declaration can have a baseType while a variant still contains an unresolved TemplateParameter.

--generated by Copilot

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The updated TextMate union-extends rules can terminate early on { inside value literals like #{...}, breaking syntax highlighting/tokenization during common error/in-progress states.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

grammars/typespec.json:1397

  • The union-extends TextMate rule ends at (?=\{), which will also match the { in value literals like #{ ... }. While union U extends #{ ... } is invalid (and correctly diagnosed), highlighting shouldn’t treat that { as the start of the union body because it can cause the remainder of the file to tokenize incorrectly while the user is typing or when viewing code with errors. Consider excluding #{ from the end condition.
    packages/compiler/src/server/tmlanguage.ts:706
  • The union-extends rule ends on any {, which causes the scope to terminate in the middle of a value literal base expression like union U extends #{ a: 1 } { ... } (a common in-progress/invalid state the checker reports as value-in-type). That { is part of the #{...} expression, but this end condition will treat it as the start of the union body and can break tokenization for the rest of the file.
  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 3, 2026 04:36
@JoshLove-msft

Copy link
Copy Markdown
Contributor Author

Investigated this in ad3f39ced4. The reported early termination does not occur: union-extends includes the expression rule, whose object-literal child begins at #{ and owns the nested braces before the parent end pattern is reconsidered. Raw TextMate scopes keep #{...} under meta.union-extends, place the following {...} under meta.union-body, and reset before the next declaration. I added a regression test covering both semantic and TextMate tokenization through a following model, so no grammar regex change is needed.

--generated by Copilot

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation is feature-gated and includes broad, targeted test coverage across parser/checker/formatter/tooling surfaces without any apparent gaps in the reviewed diffs.

Review details
  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Merged via the queue into microsoft:main with commit 64f7850 Sep 3, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:core Issues for @typespec/compiler meta:website TypeSpec.io updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implementextends base type for union

4 participants