skip to content

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%

answer

  1. three well-known design: keys
  2. only where a decorator already sits
  3. the type has to become a value
  4. one flag depends on another flag
  5. missing polyfill fails silently

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.

solid answer

~40 s

`emitDecoratorMetadata` makes the compiler attach three well-known metadata keys to declarations that already carry at least one decorator: `design:type` (the type of a property or method), `design:paramtypes` (an array of parameter types for a constructor or method), and `design:returntype`. The values are *runtime* values, so the compiler serialises the declared type — `string` becomes `String`, a class type becomes its constructor, and anything with no runtime counterpart becomes `Object`. Two conditions matter. First, the flag has no effect without `experimentalDecorators`; standard TC39 decorators get nothing from it. Second, the emitted code routes through `Reflect.metadata`, which only exists if `reflect-metadata` has been imported. Without that import the emit helper silently no-ops — no crash, just an empty metadata store, which is why the symptom is usually a confusing "cannot resolve dependency" much later.

code

typescript · 19 lines
typescript
import "reflect-metadata";

function Column(): PropertyDecorator {
  return () => {};
}

class Post {
  @Column() title!: string;
  @Column() createdAt!: Date;
  slug!: string; // no decorator -> no metadata emitted
}

for (const key of ["title", "createdAt", "slug"]) {
  const t = Reflect.getMetadata("design:type", Post.prototype, key);
  console.log(key, t?.name ?? "(none)");
}
// title String
// createdAt Date
// slug (none)

go deeper

for a junior

Know that the flag exists, that it emits type information as runtime data for decorated members, and that reading it needs the reflect-metadata package imported once at the entry point.

for a middle

Be able to name design:type, design:paramtypes and design:returntype, say that emission happens only where a decorator already is, and explain that types are serialised to runtime values with Object as the fallback.

for a senior

Demonstrate the debugging path: an empty metadata store usually means a missing polyfill import, an undecorated member, or a transpiler that cannot run the type checker. Talk about the silent no-op rather than an exception.

for a principal

Own the pipeline consequence — this feature couples your wiring to the type checker being present at build time, so transpiler choice and any future move to standard decorators are architectural commitments, not build-config details.

## What the flag is for TypeScript erases types, but decorator-driven frameworks want them: a dependency-injection container wants to know a constructor takes a `Logger`, an ORM wants to know a column holds a `Date`. `emitDecoratorMetadata` is the compiler's opt-in answer. It emits a small amount of type information as ordinary runtime data, alongside the decorators that are already there. ## The three keys For any declaration that has at least one decorator applied, the compiler emits some subset of: - `design:type` — the type of a property, or `Function` for a method. - `design:paramtypes` — an array of the parameter types of a constructor or method. - `design:returntype` — a method's declared return type. For a decorated class, `design:paramtypes` is attached to the constructor function itself and describes the constructor's parameters. For a decorated property or method, the entries are attached to the prototype under that property key. ```typescript import "reflect-metadata"; function Column(): PropertyDecorator { return () => {}; } class Post { @Column() title!: string; } const t = Reflect.getMetadata("design:type", Post.prototype, "title"); console.log(t === String); // true ``` ## The critical word: *decorated* Metadata is emitted only where a decorator already exists. An undecorated property in a decorated class gets nothing. This surprises people who expect the flag to be a global "turn on reflection" switch — it is not; it piggybacks on the decorator call the compiler is already emitting, and where there is no decorator, there is no emit site. It is also why ORM and validation libraries insist that *every* mapped member carry a decorator, even a bare marker one. ## Serialisation: the type becomes a value The metadata holds runtime values, so the compiler translates each declared type into the nearest thing that exists at runtime: - `string`, `number`, `boolean` become `String`, `Number`, `Boolean` — the wrapper constructors, not the primitives. - A class type becomes its constructor function. - An array type becomes `Array`. - Anything the type system cannot reduce to a single runtime value — an interface, a type alias for an object shape, a union of unrelated types, `any`, a generic type parameter — becomes `Object`. That last bullet is the whole ballgame for anyone using this in anger. `Object` is the compiler saying "I had nothing to give you", and it is indistinguishable from a parameter genuinely typed `object`. Generic type arguments are also dropped: a parameter typed `Repository<User>` serialises to the `Repository` constructor, with `User` nowhere in sight. ## The runtime half The emitted code does not write into some TypeScript-owned store. It calls `Reflect.metadata(key, value)`, an API from the (never-standardised) metadata reflection proposal that lives in the `reflect-metadata` package. Applications import it exactly once, at the entry point, before any decorated module is loaded: ```typescript import "reflect-metadata"; ``` If that import is missing, nothing throws. TypeScript's emit helper is guarded — it checks that `Reflect.metadata` is a function and quietly returns `undefined` if it is not. So the class defines fine, the app boots, and the failure surfaces far away as a container that cannot resolve a dependency or an ORM that thinks an entity has no columns. Recognising that silent-degradation shape is most of the debugging value of knowing this flag. ## Build-pipeline consequences Producing `design:paramtypes` requires the *type checker*, because the compiler must resolve `Logger` to a value it can emit. That means a type-stripping transpiler which deliberately works file-by-file without type information cannot implement this faithfully; toolchains that support it do so with their own approximation, and a project that switches transpilers can lose metadata without any error. If your wiring depends on this flag, the transpiler choice is a load-bearing decision, not an interchangeable one. Also note the emit is not free: every decorated declaration gains extra calls and, more importantly, forces the compiler to keep imports alive that are otherwise only used in type positions, because the metadata references them as values. ## Where it fits today `emitDecoratorMetadata` belongs strictly to the `experimentalDecorators` world. The standard TC39 decorators supported in modern TypeScript have their own metadata channel and receive nothing from this flag — a decorator there that wants types must be told them explicitly. Knowing which world a codebase is in is the first question to ask when the metadata is empty.

  • Why does a parameter typed as `Promise<User>` give you so little in design:paramtypes?
    It serialises to the `Promise` constructor and the type argument is dropped, because generic arguments have no runtime representation. The metadata can tell a library that a promise is involved but never what it resolves to — which is why frameworks that need the inner type make you pass it explicitly to the decorator factory.
  • What breaks if you enable emitDecoratorMetadata but forget to import reflect-metadata?
    Nothing visible at first. TypeScript's emit helper checks whether `Reflect.metadata` is a function and no-ops if it is not, so the class definitions evaluate normally and the metadata store stays empty. The failure appears later as unresolved injections or an entity with no mapped columns.
  • Does emitDecoratorMetadata affect declarations that have no decorator on them?
    No. The emit rides along with the decorator application, so an undecorated property, method, or class produces no metadata at all. Libraries that depend on this flag therefore require a marker decorator on every member they intend to see, which is why entity classes are decorated so exhaustively.

saying these in an interview costs you the question

  • Thinks the flag emits metadata for every declaration
  • Says design:type gives back the literal type name as a string
  • Believes interfaces survive into design:paramtypes
  • Assumes reflect-metadata ships with TypeScript
  • Claims emitDecoratorMetadata works with standard decorators

context