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?
answer
- two arguments, never a descriptor
- value plus a context object
- context knows kind, name, static, private
- return value replaces the element
- fields get undefined as the value
basics
~20 sA 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 sIn 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 linesfunction 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
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.
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.
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.
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