skip to content

Decorator Metadata & Use Cases

How decorators attach metadata that frameworks read back at runtime, and what people legitimately use them for. Expect to be asked where you would use a decorator and what it costs in readability and tooling.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

TypeScript's types are erased before the code runs, so how can a validation or ORM library that uses decorators know at runtime that a class property is supposed to hold a string?

level: juniorimportance: must knowfreq 52%

answer

  1. types vanish, code does not
  2. who is still around at runtime?
  3. decorators run when the class is defined
  4. a side table keyed by class and property
  5. design:type only with the two flags

basics

~20 s

The annotation itself is gone after compilation. Decorators are ordinary functions that run when the class is defined, so the library records what it needs in a runtime data store at that moment — either from an explicit argument or from compiler-emitted metadata.

solid answer

~40 s

A type annotation like `title: string` produces no runtime artefact at all — the emitted JavaScript has only the field. So a library cannot inspect the type; it has to be *told*. Decorators are the telling mechanism: a decorator is a normal function that executes once, at class-definition time, and it stores whatever it was given in a side table keyed by the class and property. `@IsString()` works because the string-ness is in the decorator call, not in the annotation. TypeScript can also help: with `experimentalDecorators` plus `emitDecoratorMetadata`, the compiler emits a `design:type` entry next to each decorated member, which the `reflect-metadata` library exposes. Either way the knowledge lives in runtime data the library wrote down, never in the type.

go deeper

for a junior

Be ready to say plainly that annotations are erased and that a decorator is real code that runs when the class is defined. Naming one library use case — validation or column mapping — is enough here.

for a middle

Explain the two routes a fact reaches the runtime store: passed as a decorator argument, or emitted by the compiler as design:type when experimentalDecorators and emitDecoratorMetadata are both on and reflect-metadata is loaded.

for a senior

Show that you treat decorator registration as module-load side effects: discovery depends on imports, ordering, and bundler behaviour, and a silently empty metadata store is a far more common production failure than a wrong rule.

for a principal

Own the consistency question — a hand-written decorator argument can drift from the annotation it mirrors. Argue when to derive runtime shape from a schema-first source instead, so one declaration produces both the type and the validator.

## The starting fact: nothing survives TypeScript compiles to JavaScript by deleting the type layer. An interface emits no code, a type alias emits no code, and a property annotation is stripped from the field declaration. This class: ```typescript class Post { title: string = ""; } ``` becomes, in the emitted JavaScript, a class with a `title` field initialised to `""` and nothing else. There is no object anywhere in the running program that says "`title` is a string". So any library that wants to validate, serialise, or map a property to a database column at runtime cannot read the type — the information does not exist by then. ## Decorators are runtime code A decorator is the exception to "TypeScript adds no runtime cost". `@Column()` is not an annotation the checker consumes and discards; it compiles to a real function call that runs once, when the class definition is evaluated. That gives a library a hook at exactly the moment the class comes into existence. What the library does with that hook is bookkeeping. It writes an entry into a store — a `Map`, an object hung off the constructor, or a metadata table — that says "class `Post`, property `title`, rule: must be a string". Later, when `validate(post)` or `repository.save(post)` runs, the library looks the class up in that store and finds the rules. The type system was never involved at runtime; a data structure was built during module evaluation. ## Two ways the fact gets into the store **Explicitly, as a decorator argument.** This is the common and portable route. `@IsString()`, `@Column({ type: "varchar" })`, `@MaxLength(80)` all carry the fact in the call itself. The decorator does not need to know anything about TypeScript; it would work identically in plain JavaScript. The cost is duplication: you write `title: string` for the checker and `@IsString()` for the runtime, and nothing forces the two to agree. **Implicitly, via compiler-emitted metadata.** TypeScript can close part of that gap. With both `experimentalDecorators` and `emitDecoratorMetadata` enabled, the compiler emits, for each *decorated* declaration, extra entries describing the declared types: `design:type` for a property or method, `design:paramtypes` for a constructor or method parameter list, and `design:returntype` for a method's return. These are written through `Reflect.metadata(...)`, which only exists if the `reflect-metadata` polyfill has been imported, so applications using this route import it once at startup. A library then reads it back: ```typescript import "reflect-metadata"; const t = Reflect.getMetadata("design:type", Post.prototype, "title"); console.log(t === String); // true ``` Note what had to happen for that to work: the compiler *serialised* the type into a runtime value. `string` became the `String` constructor. That is a lossy translation, and only a handful of types have a runtime value to point at. ## Why this matters even at a junior level The practical consequence is that a decorator-driven library can only enforce what someone recorded. If you change `title: string` to `title: number` and forget to change `@IsString()`, the checker and the validator now disagree and nothing warns you — unless the project is on the `emitDecoratorMetadata` route, where the emitted `design:type` follows the annotation automatically. It also explains a family of confusing bugs. A validation decorator that "stops working" is usually a decorator that never ran (the module was never imported, so the class body never evaluated), or a metadata read against the wrong target object, or a missing `reflect-metadata` import. None of these are type errors, because none of this is type-checked; it is ordinary runtime data flow that happens to be written in an unusual place. ## The bridge, stated plainly Types are a compile-time argument between you and the checker. Decorators are runtime code. Metadata is the bridge: a decorator running at class-definition time copies a fact into a runtime store so that code executing much later can act on it. Everything a decorator-based framework does — dependency injection, column mapping, request routing, validation — is that same pattern with a different table.

  • If the fact has to be written down anyway, why not just call a registration function after the class instead of using a decorator?
    You can, and some libraries do. The decorator's advantage is locality — the rule sits on the member it describes, so it cannot drift to the wrong property name during a rename. The cost is that it needs decorator support in your build, and the registration is now a hidden side effect of importing the module.
  • What happens if the module containing a decorated class is never imported?
    Nothing is registered. The class body never evaluates, so the decorators never run and the library's store has no entry for it. This is why frameworks that discover entities or handlers by decorator require you to import or glob-load every such module, and why a "missing entity" error is usually an import problem, not a configuration one.
  • Does a decorator change the declared type of the property it is attached to?
    With `experimentalDecorators`, no — the decorator's return value is applied at runtime but the checker keeps the declared type exactly as written. So anything a decorator adds or transforms at runtime is invisible to the type system, and callers see the original shape.

saying these in an interview costs you the question

  • Thinks the checker validates values at runtime
  • Says the type annotation is readable via reflection
  • Assumes decorators run on every instantiation
  • Believes design:type appears without any compiler flag
  • Confuses the decorator running with the decorated code running

context

open as a page

In TypeScript, what does the `emitDecoratorMetadata` compiler flag actually emit, and what else has to be in place for a library to read that metadata at runtime?

level: middleimportance: must knowfreq 60%

basics

~20 s

For each decorated declaration, TypeScript emits design:type, design:paramtypes and design:returntype entries describing the declared types as runtime values. The flag works only alongside experimentalDecorators, and reading the entries requires the reflect-metadata polyfill imported once at startup.

open as a page

A TypeScript service uses experimentalDecorators and emitDecoratorMetadata, and its DI container resolves constructor dependencies from design:paramtypes. One parameter is typed as an interface and the container fails to resolve it. Why does the metadata record `Object` there, and how do you make that dependency injectable?

level: seniorimportance: should knowfreq 46%

basics

~20 s

An interface has no runtime value, so the compiler serialises the parameter to Object and the container has no token to look up. Fix it by depending on a class or abstract class, or by supplying an explicit injection token through a parameter decorator.

open as a page

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%

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.

open as a page

In a TypeScript codebase, when is a decorator the right tool for something like dependency wiring, validation, or caching — and what does choosing a decorator cost you compared with a plain function or an explicit configuration object?

level: principalimportance: should knowfreq 38%

basics

~20 s

Decorators pay off for declarative, cross-cutting facts that belong next to the member they describe and are consumed by a generic runtime. They cost hidden control flow, module-load side effects, extra build configuration, harder testing, and type invisibility, since a decorator does not change the declared type.

open as a page