feat(compiler): add extends base type clause for unions - #11771
Conversation
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
commit: |
|
All changed packages have been documented.
Show changes
|
There was a problem hiding this comment.
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> { ... }, setUnion.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.
|
You can try these changes here
|
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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4518d421-bd02-4083-abc6-c5bffa94918e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 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.baseTypeis documented as guaranteeing every variant is assignable, but variant assignability checks are explicitly skipped inCheckFlags.InTemplateDeclaration(seecheckUnionVariantAgainstBaseType), whilebaseTypecan 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>
|
Addressed the Copilot review in --generated by Copilot |
There was a problem hiding this comment.
🔵 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-extendsTextMate rule ends at(?=\{), which will also match the{in value literals like#{ ... }. Whileunion 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-extendsrule ends on any{, which causes the scope to terminate in the middle of a value literal base expression likeunion U extends #{ a: 1 } { ... }(a common in-progress/invalid state the checker reports asvalue-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>
|
Investigated this in --generated by Copilot |
There was a problem hiding this comment.
🟢 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
Fixes #2737
Adds experimental
extendssupport to named union declarations.Enable the feature in
tspconfig.yamlto use it without an experimental warning:Semantics
extendson a union is purely an assignability constraint:Union.baseType, giving emitters an easy way to represent the union with a polymorphic base type in languages that do not support unions natively.@discriminator.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),
extendsremains 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
union-extendscompiler feature and reportsexperimental-featurewhen it is not enabledUnionStatementNode.extendsand narrowsUnion.baseTypetoModel | Scalar | Enum | UnionUnion.baseTypeCircular references
union a extends aand alias indirection are caught by the existingpendingResolutions/ResolutionKind.BaseTypemechanism.Because a union constraint can also be a union expression, references such as
union a extends a | stringare detected on the resolved type. That walk deliberately follows only union expressions, so legal cyclic data graphs remain accepted:Templates
Variant assignability is skipped inside an uninstantiated template declaration. Each instantiation is checked, and each valid instantiation gets its own concrete
baseType.Validation
@typespec/compilersuite: 4,196 passed, 6 skipped