feat(compiler): allow declarations to be used as expressions#11019
feat(compiler): allow declarations to be used as expressions#11019timotheeguerin wants to merge 10 commits into
Conversation
Allow model, enum, union, and scalar declarations to be used in expression position (e.g. alias RHS, property types). In expression position they are anonymous (name is "") and the resulting type has expression: true; they are not registered in the enclosing namespace. A diagnostic is reported when template parameters are used on a declaration in expression position.
@typespec/compiler
@typespec/html-program-viewer
@typespec/json-schema
@typespec/openapi
@typespec/openapi3
@typespec/versioning
commit: |
|
All changed packages have been documented.
Show changes
|
|
You can try these changes here
|
A keyword-form union (`union { a, b }`) used in expression position is marked
`expression: true`, which caused checkUnionExpression to flatten its (possibly
named) variants into the parent union, silently dropping colliding members.
Flatten only unions originating from the `|` operator (UnionExpression node).
Add tests for: - expression: false on statement declarations - name retention on named declaration expressions - named expressions not being referenceable - union namespace non-registration - alias-resolved types, op return/param, union variant usage - member access via alias, decorator rejection - enum values, union named variants, scalar constructors, model spread - parser negatives for interface/op in expression position - formatter named & nested declaration expressions
Anonymous declarations used in expression position rendered with a stray
namespace prefix (e.g. `Ns.` for enum/scalar, `Ns.{ x: string }` for
keyword-form model). Render them inline and un-prefixed, mirroring union
expression naming.
Also extract a single shared `isDeclarationInExpressionPosition` helper used
by both the binder and checker so the two position predicates cannot drift,
and add regression tests (type names, keyword-form union as `|` operand,
template parameter referenced inside an expression declaration).
Inline anonymous declaration expressions and hoist named ones across the OpenAPI and JSON Schema emitters, validate keyword-form union expression variants in versioning, and derive the enum typekit `expression` flag from an empty name.
Summary
Allow
model,enum,union, andscalardeclarations to be used as expressions (e.g. the right-hand side of analias, property types, return types, union variants).In expression position these declarations:
nameis""expression: true(so the type graph explicitly records that it came from an expression)Named forms in expression position are also allowed (e.g.
nested: model Inner { x: string }), and declarations can be nested.Motivation
A model/enum/scalar is often only meaningful in the context of its parent type. Allowing them inline lets a spec be expressed more clearly without polluting the namespace with single-use declarations. Specifically this enables:
model Inner { ... }documents intent and gives emitters a usable name, even though the declaration is not registered or referenceable.decoratorsfield; a later PR can allow@doc("...") model Inner { ... }in expression position.All four data-shape declarations are supported for consistency.
interface,op, andaliasare intentionally excluded — an anonymous interface/op has no meaningful use as a type expression.Approach
Rather than introducing new
*ExpressionAST node kinds, the existing statement node kinds are reused with an optionalid(OptionallyNamedDeclarationNode), and these kinds are added to theExpressionunion. Whether a declaration is in statement vs expression position is determined by its parent node kind.OptionallyNamedDeclarationNode(optionalid);expression: booleanonModel/Enum/Scalar(Unionalready had it); the 4 statement kinds added to theExpressionunion.parsePrimaryExpressiondispatch for themodel/enum/union/scalarkeywords; ascalarin expression position no longer consumes the alias trailing;."-"symbol) vs named binding depending on position.name/expression; skip namespace registration for expression-form types; route expression decls through the namespace walk-up ingetParentNamespaceType; newtemplated-declaration-in-expressiondiagnostic.;for anonymous scalars.id.Semantics of the
expressionflagexpressionmeans "produced in expression position / not registered as a named statement" — it is position-based, not name-based. A named declaration expression (model Inner { ... }used as a property type) is thereforeexpression: trueeven though it has a name. This is the single definition all consumers should rely on.Diagnostics
Template parameters on a declaration in expression position are rejected (
templated-declaration-in-expression) since an anonymous declaration cannot be referenced or instantiated.Tests
test/checker/declaration-expressions.test.ts(14 tests)test/parser.test.ts— new "declaration expressions" block (11 cases)test/formatter/formatter.test.ts— new "declaration expressions" block (5 cases)Full compiler suite: 3982 passed / 6 skipped.
tsc --noEmit,pnpm format, andpnpm lintare all clean.Follow-ups (not in this PR)
@handling inparsePrimaryExpressionis narrowly relaxed to "decorators immediately followed by a declaration keyword" (keyword-only — do not broaden to arbitrary expressions). Decorators attach to the keyword-form node.Type.namebut is not referenceable. Whether it should ever be bindable is still undecided.expressionflag (expression= "produced in expression position / not registered as a named statement"):packages/openapi/src/helpers.tsshouldInline→ inline whentype.expression(else anonymous scalars emit empty-named declarations and named expressions are hoisted as collision-prone components).packages/versioning/src/validate.tsunion-variant branch → gate onSyntaxKind.UnionExpression, notexpression(keyword-form union variants can have decorators).kits/enum.ts(expression: false) withkits/model.ts(name === undefined).Declarationnow admits an optionalid(public type) — minor downstream typing impact.