skip to content

With standard (TC39) decorators in TypeScript 5.2 and later, how does a decorator record metadata about a class, and how does a library read that metadata back once the class is defined?

level: seniorimportance: should knowfreq 30%

answer

  1. one object per class, shared by all decorators
  2. the class carries its own description
  3. a well-known symbol, not a side table
  4. nothing here knows any types
  5. polyfill plus an esnext lib

basics

~20 s

Each standard decorator receives a context object with a metadata property — one shared object per class that all its decorators write into. When decoration finishes, TypeScript installs that object on the class under Symbol.metadata, where any library can read it.

solid answer

~40 s

Standard decorators take `(value, context)`, and `context.metadata` is an ordinary object shared by every decorator applied to that class and its members. A decorator writes whatever it wants there — a list of serialisable fields, a validation table — and once decoration completes the object is installed on the constructor under the well-known `Symbol.metadata` key, so a library reads it with `SomeClass[Symbol.metadata]`. Two practical requirements: `Symbol.metadata` needs a polyfill in runtimes that lack it, typically `(Symbol as any).metadata ??= Symbol("Symbol.metadata")`, and the type declarations need the `esnext.decorators` lib. The important limitation is that nothing here carries type information. `emitDecoratorMetadata` applies only to `experimentalDecorators`, so there is no `design:paramtypes` equivalent — a standard decorator that needs to know a member's type must be told explicitly.

code

typescript · 16 lines
typescript
// tsconfig: "target": "es2022", "lib": ["es2022", "esnext.decorators"]
(Symbol as { metadata?: symbol }).metadata ??= Symbol("Symbol.metadata");

function serialize(_value: undefined, context: ClassFieldDecoratorContext): void {
  const meta = context.metadata as Record<string, string[]>;
  (meta.fields ??= []).push(String(context.name));
}

class User {
  @serialize name = "";
  @serialize email = "";
  password = "";
}

const meta = User[Symbol.metadata] as Record<string, string[]>;
console.log(meta.fields); // [ "name", "email" ]

go deeper

for a junior

Know that standard decorators receive a context object and that context.metadata is where a decorator can leave notes about the class for a library to read later.

for a middle

Explain that the metadata object is shared across all decorators on one class and is installed on the constructor under Symbol.metadata once decoration completes, and name the polyfill and lib requirements.

for a senior

Demonstrate that no type information is available here — emitDecoratorMetadata does not apply — so a library must be told types explicitly, and show how you would namespace keys to survive alongside another library.

for a principal

Own the tradeoff: this channel is portable and flag-free but deliberately type-blind, so decide whether your framework's contracts should be declared in the decorator call or derived from a schema that also produces the types.

## The shape of the channel A standard decorator is called with two arguments: the thing being decorated and a context object describing it. Decorator metadata, supported in TypeScript from 5.2, adds one more member to that context: `context.metadata`. The crucial property is *sharing*. Every decorator applied anywhere in a single class — on the class itself, on its methods, on its fields, on its accessors — receives the same `metadata` object. That makes it a natural accumulator: a field decorator pushes the field's name into a list, and a class decorator applied afterwards can read the completed list. ```typescript function serialize(_value: undefined, context: ClassFieldDecoratorContext): void { const meta = context.metadata as Record<string, string[]>; (meta.fields ??= []).push(String(context.name)); } ``` ## Reading it back When decoration of the class finishes, the runtime installs that object on the class under the well-known symbol `Symbol.metadata`. Any library that holds a reference to the class can then read it without cooperating with the decorators at all: ```typescript const meta = User[Symbol.metadata]; ``` That is the whole protocol, and its simplicity is the point. There is no side registry, no global `WeakMap` the library must own, and no import-order dependency between the decorator module and the reader. The data hangs off the class, so it travels with the class. Subclassing is handled by the prototype chain: the metadata object created for a derived class is created with the base class's metadata object as its prototype, so entries recorded on the base are visible through the derived class's object while the derived class's own writes shadow them rather than mutating the base. If you need only a class's *own* entries, use `Object.hasOwn` rather than plain property access. ## What you must set up Two pieces of plumbing catch people out. First, `Symbol.metadata` is a well-known symbol that a runtime may not yet define. The conventional polyfill is a single line at the entry point: ```typescript (Symbol as { metadata?: symbol }).metadata ??= Symbol("Symbol.metadata"); ``` Second, the type declaration for `Symbol.metadata` lives in the `esnext.decorators` lib file, so the tsconfig `lib` array must include it (or a lib that pulls it in). Without it the checker does not believe `Symbol.metadata` exists, and the `DecoratorMetadata` type resolves to `undefined`, which produces baffling errors on `context.metadata`. ## The limitation that matters most There is no type information in any of this. `emitDecoratorMetadata` is bound to `experimentalDecorators`; it contributes nothing to standard decorators, and there is no `design:type` or `design:paramtypes` equivalent in the standard channel. Everything in `context.metadata` is what a decorator explicitly put there. That pushes libraries toward declaring types in the decorator call, which is the honest design anyway — `@column({ type: "varchar" })` rather than a compiler flag inferring `String` from an annotation and getting `Object` for anything interesting. It costs some duplication and gains portability: a decorator written this way needs no compiler flag, no polyfill for reflection, and no `reflect-metadata` dependency, and it behaves identically under any conforming toolchain. It is also why frameworks built on constructor-parameter injection cannot simply move over. Standard decorators have no parameter decorators at all, and there is no serialised parameter type list, so the two things that made that style work are both absent. ## When to reach for it Use `context.metadata` when you want a class to carry a small, declarative description of itself that a generic routine reads later: which fields serialise, which methods are routes, which properties need validation. Keep the entries plain and serialisable, namespace your keys so two independent libraries decorating the same class do not collide (a module-scoped `Symbol` as the key is the safest choice), and never assume anything about a key you did not write. Treat the object as a shared public surface on the class, because that is exactly what it is.

  • Two independent libraries decorate the same class. How do you stop their metadata from colliding?
    Key your entries with a module-scoped `Symbol` rather than a string. The metadata object is a shared public surface on the class, so string keys like `fields` are genuinely at risk of being overwritten by another library. A symbol key is unforgeable, and you should read only keys you wrote.
  • How does a subclass see metadata recorded on its base class?
    The derived class's metadata object is created with the base's metadata object as its prototype, so base entries are visible through ordinary property lookup while the derived class's own writes shadow them. If you need only what this class declared, check with `Object.hasOwn` instead of reading the property directly.
  • Can a standard decorator find out that a decorated field was declared as `string`?
    No. The type is erased and emitDecoratorMetadata does not apply to standard decorators, so there is no design:type equivalent. The decorator must be told — typically as an argument to a decorator factory, which is more verbose but portable, flag-free and unambiguous for interfaces and unions.

saying these in an interview costs you the question

  • Expects design:paramtypes to work with standard decorators
  • Thinks reflect-metadata is required for context.metadata
  • Assumes each decorator gets its own metadata object
  • Says metadata is readable before the class finishes decorating
  • Uses generic string keys and ignores collision risk

context