Skip to content

Data decorators#10197

Draft
timotheeguerin wants to merge 5 commits intomicrosoft:mainfrom
timotheeguerin:data-decorators
Draft

Data decorators#10197
timotheeguerin wants to merge 5 commits intomicrosoft:mainfrom
timotheeguerin:data-decorators

Conversation

@timotheeguerin
Copy link
Copy Markdown
Member

@timotheeguerin timotheeguerin commented Mar 30, 2026

Data Decorators

Summary

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

data 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). Data decorators eliminate this entirely.

Design

Syntax

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

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

Storage

The compiler auto-generates an implementation that stores args in program.stateMap keyed by Symbol.for("data-dec:<fqn>"):

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

Compiler API

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

import { hasDataDecorator, getDataDecoratorValue } from "@typespec/compiler";

hasDataDecorator(program, "MyLib.label", type);           // boolean
getDataDecoratorValue(program, "MyLib.label", type);      // unknown | undefined
getDataDecoratorTargets(program, "MyLib.label");           // Map<Type, unknown>

tspd generation

tspd gen-extern-signature generates typed accessors for data decorators:

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

// For: data 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" | "data" property (extensible for future kinds).

data as a contextual keyword

data is a modifier keyword but remains usable as an identifier (decorator names, property names, etc.) to avoid breaking existing code like @data in @typespec/events.

Open Questions

  • Modifier name: Is data the right keyword? Alternatives considered: metadata, declare, attr, pure. data was chosen for brevity and precedent (Kotlin data class), but open to alternatives.

  • Multiple data decorators on the same node: Should multiple different data decorators be allowed on the same target? Currently there's no restriction — each stores independently. Should we add validation or a way to declare exclusivity?

  • Behavior with model is / op is: When a model or operation copies from another via is, should data decorator metadata be inherited by the copy? Currently decorators applied via is are re-applied, so data decorators would also be re-applied. Is this the desired behavior, or should data decorators have different inheritance semantics?

Migration Examples

Several existing decorators across the TypeSpec ecosystem are pure "store args in state" and could be migrated to data 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:

data 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:

data dec body(target: ModelProperty);

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

OpenAPI — @operationId

Before:

extern dec operationId(target: Operation, operationId: valueof string);
export const $operationId: OperationIdDecorator = (context, entity, opId) => {
  setOperationId(context.program, entity, opId);
};

After:

data dec operationId(target: Operation, operationId: valueof string);

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
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 30, 2026

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: c6e831c

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

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 data decorator modifier for declaring decorators that auto-store their arguments as metadata without requiring a JavaScript implementation.,> ,> typespec,> data dec label(target: Model, value: valueof string);,> ,> @label("my-model"),> model Foo {},> ,> ,> Added compiler API hasDataDecorator, getDataDecoratorValue, and getDataDecoratorTargets for reading data decorator values by FQN.

@typespec/tspd - feature ✏️

tspd gen-extern-signature now generates typed accessor functions for data 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
Copy link
Copy Markdown
Collaborator

azure-sdk commented Mar 30, 2026

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.

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