Skip to content

Auto decorators#10197

Draft
timotheeguerin wants to merge 13 commits into
microsoft:mainfrom
timotheeguerin:data-decorators
Draft

Auto decorators#10197
timotheeguerin wants to merge 13 commits into
microsoft:mainfrom
timotheeguerin:data-decorators

Conversation

@timotheeguerin

@timotheeguerin timotheeguerin commented Mar 30, 2026

Copy link
Copy Markdown
Member

Auto decorators

Summary

Adds an auto modifier for decorator declarations. Auto decorators are simple metadata annotations that auto-store their arguments — no JavaScript implementation needed.

auto dec label(target: Model, value: valueof string);

@label("my-model")
model Foo {}

Motivation

A large number of decorators in the TypeSpec ecosystem follow the same pattern: accept arguments, store them in a state map, expose a getter. This requires boilerplate across three files (.tsp declaration, .ts implementation, .ts state accessors). Auto decorators eliminate this entirely.

Design

Syntax

auto is a new modifier keyword on dec declarations, mutually exclusive with extern:

auto dec myFlag(target: Model);                                    // boolean flag
auto dec myLabel(target: Model, label: valueof string);            // single value
auto dec myMeta(target: Model, name: valueof string, v: valueof int32); // multi-arg
internal auto dec myInternal(target: Model);                       // combinable with internal

Auto decorators are gated behind the auto-decorators compiler feature. Declaring one without enabling the feature in tspconfig.yaml is an error.

Storage

The compiler auto-generates an implementation that stores args in program.stateMap keyed by Symbol.for("TypeSpec.dec:<fqn>"). The value is always a record keyed by parameter name (this uniformity keeps the generic read API simple and enables seamless migration to/from an extern implementation that shares the same state key):

Parameters (beyond target) Stored value
None {}
One { paramName: value }
Multiple { paramName1: val1, paramName2: val2 }

Applying the same auto decorator twice on the same declaration emits a duplicate-decorator warning (via the shared validateDecoratorUniqueOnNode helper) but still stores — duplicate resolution is last-write-wins, matching extern decorators.

Compiler API

Generic functions to read auto decorator state by FQN string — no generated code needed:

import { hasAutoDecorator, getAutoDecoratorValue } from "@typespec/compiler";

hasAutoDecorator(program, "MyLib.label", type);           // boolean
getAutoDecoratorValue(program, "MyLib.label", type);      // the raw record, or undefined
getAutoDecoratorTargets(program, "MyLib.label");          // Map<Type, unknown>

getAutoDecoratorValue always returns the raw stored record (e.g. { value: "x" }).

tspd generation

tspd gen-extern-signature generates typed accessors for auto decorators. These are ergonomic sugar over the generic API: the single-arg get* unwraps the record to the bare value so it stays a drop-in replacement for hand-written extern getters, multi-arg returns the record, and no-arg returns a boolean is*:

// For: auto dec myFlag(target: Model)
export function isMyFlag(program: Program, target: Model): boolean;

// For: auto dec myLabel(target: Model, label: valueof string)
export function getMyLabel(program: Program, target: Model): string | undefined;

Decorator type

The Decorator runtime type gains a declarationKind: "extern" | "auto" property (extensible for future kinds).

auto as a modifier keyword

auto is a modifier keyword on dec declarations.

Open Questions

  • Behavior with model is / op is: Auto decorators inherit through is exactly like extern decorators (decorators are re-applied to the copy); no special-casing.

Migration Examples

Several existing decorators across the TypeSpec ecosystem are pure "store args in state" and could be migrated to auto dec:

Compiler — @discriminator

Before:

extern dec discriminator(target: Model | Union, propertyName: valueof string);
export const $discriminator: DiscriminatorDecorator = (context, entity, propertyName) => {
  setDiscriminator(context.program, entity, { propertyName });
};

After:

auto dec discriminator(target: Model | Union, propertyName: valueof string);

No JS needed.

HTTP — @body, @bodyRoot, @bodyIgnore, @statusCode, @multipartBody

These are all boolean flags — the decorator just marks the target:

Before:

extern dec body(target: ModelProperty);
export const $body: BodyDecorator = (context, entity) => {
  context.program.stateSet(HttpStateKeys.body).add(entity);
};

After:

auto dec body(target: ModelProperty);

Same pattern applies to @bodyRoot, @bodyIgnore, @statusCode, @multipartBody.

Not candidates

Decorators that do validation, normalization, or side effects beyond storage are not candidates. Examples: @doc (template string rewriting), @format (uniqueness validation), @pattern (regex validation), @route (shared route handling), @server (URL parameter parsing).

@microsoft-github-policy-service microsoft-github-policy-service Bot added compiler:core Issues for @typespec/compiler tspd Issues for the tspd tool labels Mar 30, 2026
@pkg-pr-new

pkg-pr-new Bot commented Mar 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@10197
npm i https://pkg.pr.new/@typespec/html-program-viewer@10197
npm i https://pkg.pr.new/@typespec/tspd@10197

commit: b8eb823

@github-actions

github-actions Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/compiler
  • @typespec/html-program-viewer
  • @typespec/tspd
Show changes

@typespec/html-program-viewer - internal ✏️

Data decorators

@typespec/compiler - feature ✏️

Added auto decorator modifier for declaring decorators that auto-store their arguments as metadata without requiring a JavaScript implementation.,> ,> typespec,> auto dec label(target: Model, value: valueof string);,> ,> @label("my-model"),> model Foo {},> ,> ,> Added compiler API hasAutoDecorator, getAutoDecoratorValue, and getAutoDecoratorTargets for reading auto decorator values by FQN.

@typespec/tspd - feature ✏️

tspd gen-extern-signature now generates typed accessor functions for auto decorators (e.g., isMyFlag, getMyLabel).

@microsoft-github-policy-service microsoft-github-policy-service Bot added the meta:website TypeSpec.io updates label Mar 30, 2026
@azure-sdk

azure-sdk commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@witemple-msft

Copy link
Copy Markdown
Member

I generally like the idea of the feature but I have a couple of reservations:

  1. I generally think that relying on constructing a symbol by stringly-typed convention is just too brittle for the compiler API boundary. The generic read APIs for data decorators take FQN as a string, the symbol for storage in the program state map is data-dec:${fqn}. That's super simple, and it does seem stable for now since we have no way to alias decorators. But imagine if we had a way to alias a decorator, just moving a decorator from one namespace to another and preserving a backcompat alias would be an "ABI" breaking change to the metadata interface for that decorator. I think it would probably be best if the symbol for the decorator was embedded into the Decorator Type instance as well, and we could use the Decorator identity rather than its fully-qualified name to extract bound metadata. For visibility, I used Enum identity for a similar purpose, rather than the enum FQN. That said, it's much easier to get a reference to an enum than it is to get a reference to a decorator in library code.

  2. I think three storage shapes depending on the decorator's arity is a real problem for programming contract stability. The arity-based thing does seem like it would be convenient, but I think a regular shape would age better and be easier to analyze with tooling. If the storage shape changes by arity, and you add an argument, even an optional one, suddenly you are changing the ABI for accessing that decorator's metadata which could break a lot of callers.

  3. I'm really not so sure about data as a modifier name. I do see the similarity to data class, though it is still quite different. Is there something that more clearly communicates the idea of "compiler-managed metadata binding". Maybe just auto, but that also comes with its own baggage from other languages.

On the questions:

  • See no. 3 above for modifier name.
  • I don't see any reason that data decorators specifically should have any particular exclusivity criteria. If we wanted to have some way to declaratively apply exclusivity I think it would apply just as well to other kinds of decorators, so IMO that question is orthogonal to the design of data decorators except that it would be the only way to do it for data decorators, while manually implemented decorators can of course check exclusivity themselves. Furthermore, this seems likely to be a library-policy concern, so i don't think we could come up with any particular rule that would make sense to enforce at the language level, so it would have to be declarative syntax (decorators of decorators) used to configure exclusivity behavior.
  • For the same reason, I think the question about applicability through is inheritance is also orthogonal. If we had a way to declaratively state that a decorator does not apply by transition through is relationships, then we would want that to apply to extern decorators as well. So I think as long as we're talking just about the specifics of data decorators, I don't think we want them to do anything unique or special compared to extern decorators from an applicability perspective.

@timotheeguerin

Copy link
Copy Markdown
Member Author

Design meeting descision:

  • Use auto instead of data
  • Always save value as key value pair(parameter name, value)
  • Warn on duplicate decorator on the same node

@microsoft-github-policy-service microsoft-github-policy-service Bot added the stale Mark a PR that hasn't been recently updated and will be closed. label May 4, 2026
@timotheeguerin timotheeguerin removed the stale Mark a PR that hasn't been recently updated and will be closed. label Jun 8, 2026
@timotheeguerin timotheeguerin changed the title Data decorators Auto decorators Jun 15, 2026
…plicate handling

- Single-arg tspd typed accessor now unwraps the stored record to the bare
  value, preserving parity with hand-written extern getters
- Auto decorator duplicate detection reuses validateDecoratorUniqueOnNode
  (identity match, warning) and no longer early-returns, so duplicates are
  last-write-wins like extern decorators
- Add tests: hasAutoDecorator true/false, getAutoDecoratorValue undefined
  when absent, `is` inheritance, non-Model (ModelProperty) target, auto
  rejected on model/function declarations, namespaced tspd accessor FQN
- Restrict function declarations to extern|internal modifiers so the auto
  modifier is no longer silently accepted on functions
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 tspd Issues for the tspd tool ui:type-graph-viewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants