skip to content

Standard (TC39) Decorators

The ECMAScript decorators TypeScript 5 supports without any flag: a function taking the decorated value plus a context object. Interviewers ask about the context object and initializer hooks to check you have moved past the old experimental form.

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

questions

5

In TypeScript 5, a standard (TC39) decorator function is called with two arguments. What are they, and what does the function's return value do when it decorates a class method?

level: juniorimportance: must knowfreq 70%

answer

  1. two arguments, never a descriptor
  2. value plus a context object
  3. context knows kind, name, static, private
  4. return value replaces the element
  5. fields get undefined as the value

basics

~20 s

A standard decorator receives the decorated value — the method, accessor or class itself — and a context object carrying kind, name, static, private, access and addInitializer. Whatever it returns replaces the decorated element; returning nothing leaves it untouched.

solid answer

~40 s

In TypeScript 5 a standard decorator is a plain function called once while the class is being defined, with the signature `(value, context)`. `value` is the thing being decorated: the constructor for a class decorator, the function itself for a method, getter or setter, a `{ get, set }` pair for an `accessor` member, and `undefined` for a plain field, because a field has no value yet at definition time. `context` describes the element — `kind` ("class", "method", "getter", "setter", "field" or "accessor"), `name`, `static`, `private`, an `access` helper for reading or writing the element on an instance, and `addInitializer`. For a method, returning a function installs that function in place of the original, which is how wrapping decorators are written; returning `undefined` keeps the original method.

code

typescript · 19 lines
typescript
function logged<T extends (this: any, ...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext,
): T {
  const label = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`entering ${label}`);
    return target.call(this, ...args);
  } as T;
}

class Greeter {
  @logged
  greet(who: string) {
    return `hi ${who}`;
  }
}

console.log(new Greeter().greet("ann"));

go deeper

for a junior

Be able to write the two-parameter signature from memory and say what the second parameter carries: kind, name, static, private, access, addInitializer. Say plainly that returning a function from a method decorator replaces the method.

for a middle

Explain how the first argument changes per target — constructor, function, get/set pair, or undefined for a field — and why a field gets nothing. Name the lib types such as ClassMethodDecoratorContext that enforce the return shape.

for a senior

Show judgment about the runtime cost: decorators execute at module evaluation, once per element, so heavy work there slows startup. Explain why wrapping via the return value is safer than reaching into the prototype yourself.

for a principal

Own the API-design tradeoff of exposing behaviour through decorators at all: they are invisible at the call site, they bind consumers to a runtime-code feature rather than an erased type, and a plain higher-order function or explicit composition is often the cheaper contract.

## The shape in one sentence A standard (TC39) decorator in TypeScript 5 is an ordinary function that the runtime calls **once, while the class is being defined**, with two arguments: the value being decorated and a context object describing it. There is no descriptor, no separate property-key argument, and no compiler flag required — TypeScript 5.0 shipped support for the ECMAScript decorators proposal enabled by default. ```typescript function noop(value: unknown, context: DecoratorContext) { return value; } ``` ## The first argument: the decorated value What lands in `value` depends on what you attached the decorator to: - **class** — the constructor function itself. - **method** — the function that is about to be installed on the prototype (or on the constructor for a `static` method). - **getter / setter** — that one accessor function, not a pair. - **auto-accessor** (`accessor x = 1`) — an object with `get` and `set` functions that read and write the hidden backing storage. - **field** — `undefined`. A field has no value at class-definition time; every instance produces its own value later, so there is nothing to hand you. ## The second argument: the context object The context is a plain object whose useful members are: - `kind` — the string `"class"`, `"method"`, `"getter"`, `"setter"`, `"field"` or `"accessor"`. One decorator function can serve several targets by branching on it. - `name` — a `string` or `symbol` for members; for a class it is the class name or `undefined` for an anonymous class expression. - `static` — whether the member lives on the constructor rather than the prototype. - `private` — whether it is a `#`-private member. Private members cannot be reached by name from outside, which is why the context supplies… - `access` — an object with `has`, and `get` and/or `set` depending on the kind, so a decorator can read or write the element on a given object even when it is private. - `addInitializer(fn)` — registers a callback to run later, when instances (or the class, for statics) are initialized. ## What the return value means The return value is how a decorator changes anything; standard decorators are not supposed to mutate the prototype behind the compiler's back. - **method / getter / setter** — return a function to replace the original. This is the wrapping idiom: capture `value`, return a new function that calls it with `Reflect.apply` or `value.call(this, ...)`. - **class** — return a class (usually a subclass or the same one) to replace the binding. - **field** — return `(initialValue) => newValue`, an initializer transform run per instance. - **auto-accessor** — return `{ get?, set?, init? }`. - Returning `undefined` from any of them means "leave it as it was", which is what a decorator that only calls `addInitializer` does. Returning the wrong shape is a compile error, because the standard library types encode each contract: `ClassDecoratorContext`, `ClassMethodDecoratorContext`, `ClassGetterDecoratorContext`, `ClassSetterDecoratorContext`, `ClassFieldDecoratorContext` and `ClassAccessorDecoratorContext` all ship in TypeScript's own lib files, and the union `DecoratorContext` covers them all. ## Decorators are real runtime code Most of TypeScript's type layer is erased — an `interface` emits nothing, an `as` assertion emits nothing. Decorators are one of the few constructs that **do** emit. The decorator function is a value that exists at runtime, and it is invoked once per decorated element when the module containing the class is evaluated. That has a real cost and a real ordering: a class with twenty decorated methods performs twenty calls at load time, and any side effect you put in the decorator body happens then, not when a method is called. ```typescript function logged<T extends (this: any, ...args: any[]) => any>( target: T, context: ClassMethodDecoratorContext, ): T { const label = String(context.name); return function (this: any, ...args: any[]) { console.log(`entering ${label}`); return target.call(this, ...args); } as T; } ``` ## Traps worth naming - **Expecting a property descriptor.** The older experimental decorator form had a completely different signature; if you reach for `descriptor.value`, you are writing the wrong feature. - **Mutating instead of returning.** Assigning onto the prototype from inside a decorator may appear to work but fights the contract; return the replacement. - **Expecting `this` to be an instance.** The decorator body runs before any instance exists. Per-instance work belongs in an `addInitializer` callback or an initializer transform. - **Expecting it to run per call.** The decorator runs once; the *replacement* it returns is what runs per call.

  • How would one decorator function support being placed on both a method and a class?
    Branch on `context.kind`. The context object's `kind` field is a string literal union, so `if (context.kind === "method")` narrows both the context and, with a properly typed overload or a `DecoratorContext` parameter, tells you what shape `value` has and what you are allowed to return. Without that check you risk returning a replacement function where a class was expected.
  • What is `context.access` for, given you already know the member's name?
    `name` is useless for a `#`-private member — you cannot index an object with it from outside the class body. `access` closes that gap: it hands you `has`, plus `get` and/or `set` functions compiled inside the class, so a decorator can read or write the element on any object you pass it, private or not.
  • Does a decorator run again for every instance you construct?
    No. The decorator function itself runs exactly once per decorated element, when the class definition is evaluated. What repeats per instance is anything it registered for later: a field initializer transform it returned, or a callback it passed to `addInitializer`. Putting expensive per-instance work directly in the decorator body simply never happens more than once.

saying these in an interview costs you the question

  • Says the decorator receives a property descriptor to mutate
  • Thinks the decorator body runs on every method call
  • Expects `this` inside the decorator to be an instance
  • Claims a field decorator receives the field's initial value
  • Believes decorators are erased like the rest of the type layer

context

open as a page

In a TypeScript class body, what does declaring a member with the `accessor` keyword — `accessor name = 'ann'` — actually create, and what does a standard (TC39) decorator on it receive and return?

level: middleimportance: should knowfreq 38%

basics

~20 s

An auto-accessor creates a getter/setter pair backed by hidden private storage instead of a plain data property. Its decorator receives an object with get and set functions and may return replacements plus an init function that transforms the starting value.

open as a page

In TypeScript 5, a standard (TC39) decorator placed on a class field is called with `undefined` as its first argument. Why, and what can such a decorator actually change about the field?

level: middleimportance: should knowfreq 45%

basics

~20 s

A field has no value when the class is defined, so nothing can be passed. A field decorator instead returns an initializer transform, a function taking each instance's initial value and returning the value actually stored, with this bound to the instance.

open as a page

In TypeScript 5, a standard (TC39) method decorator returns a replacement function that is installed once on the prototype. So how do you use a decorator to give each instance its own bound copy of that method, and what exactly does `context.addInitializer` run and when?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Use context.addInitializer. It registers a callback that runs at the start of each instance's construction with this bound to that instance, so the decorator can assign a bound copy of the method as an own property. A returned replacement is shared and cannot do that.

open as a page

In TypeScript 5, given `@outer @inner greet() {}` on a class method, in what order are the two decorator expressions evaluated versus applied, and where do class decorators fall relative to member decorators?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Decorator expressions evaluate top to bottom in source order when the class is defined; the resulting functions are then applied bottom up, so the one nearest the declaration wraps first. Member decorators are applied before class decorators, which see the finished class.

open as a page