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?
answer
- returned replacement lives on the prototype
- needs a per-instance hook
- addInitializer, this is the instance
- runs at the start of the constructor
- before field initializers, so fields are undefined
basics
~20 sUse 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.
solid answer
~50 sA method decorator's return value replaces the prototype method, and a prototype method is shared by every instance, so nothing you return can be per-instance. `context.addInitializer(fn)` is the hook for that: the callback runs during initialization — at the beginning of the constructor for an instance member, during class initialization for a `static` one — with `this` bound to the instance (or the class). Inside it you can assign an own property, so an auto-binding decorator reads the prototype method through `this[context.name]`, calls `.bind(this)`, and writes the result back onto the instance, shadowing the shared one. Write the callback as a `function`, not an arrow, or `this` is not the instance. Two timing facts matter: it runs before the instance's field initializers, so do not read fields there, and a throw inside it aborts construction. A class decorator's `addInitializer` runs later, after the class is fully defined, with `this` as the class.
code
typescript · 20 linesfunction bound(_target: Function, context: ClassMethodDecoratorContext) {
const name = context.name;
context.addInitializer(function (this: any) {
this[name] = this[name].bind(this);
});
}
class Timer {
count = 0;
@bound
tick() {
this.count += 1;
}
}
const t = new Timer();
const detached = t.tick;
detached();
console.log(t.count); // 1go deeper
Know that a decorator can register a callback with context.addInitializer, and that for a normal method this runs for every instance with this pointing at that instance.
Explain why the return value cannot help: it replaces one shared prototype function. Then describe the binding pattern — read the method through this, bind it, assign it back as an own property.
Demonstrate the timing judgment: the callback runs ahead of field initializers, an arrow function breaks this, and a throw there fails construction. Say when you would reach for a plain wrapper instead.
Own the cost and clarity tradeoff of hiding per-instance setup in decorators at all — extra allocation per instance, behaviour invisible at the call site, harder debugging — and set a team rule for when the ergonomics justify it.
## The problem ```typescript class Timer { count = 0; tick() { this.count += 1; } } const t = new Timer(); const detached = t.tick; detached(); // this is not the instance ``` The usual fix is a bound copy per instance. Can a method decorator do it? Its lever is the return value, and that replaces the function installed on the **prototype** — one function object shared by every instance. It has no instance to bind to, because at class-definition time none exists. So the return value is structurally the wrong tool. ## What `addInitializer` is Every standard decorator context exposes `addInitializer(fn)`. It does not change the decorated element; it schedules `fn` to run during initialization: - **non-static member decorator** — `fn` runs at the beginning of the constructor of every instance, with `this` bound to that instance. - **static member decorator** — `fn` runs once during class initialization, with `this` bound to the class. - **class decorator** — `fn` runs after the class has been fully defined, with `this` bound to the class. This is the natural place to register the class somewhere. A decorator may call it more than once, and may both call it and return a replacement. ## The auto-binding decorator ```typescript function bound(_target: Function, context: ClassMethodDecoratorContext) { const name = context.name; context.addInitializer(function (this: any) { this[name] = this[name].bind(this); }); } class Timer { count = 0; @bound tick() { this.count += 1; } } const t = new Timer(); const detached = t.tick; detached(); t.count; // 1 ``` The read `this[name]` finds the method on the prototype, `.bind(this)` produces a closure permanently tied to this instance, and the assignment creates an **own** property that shadows the prototype one. Note the decorator itself returns nothing: it changes nothing about the shared method. ## Four details that separate a correct answer from a vague one **1. `function`, not an arrow.** The callback's `this` is supplied by the runtime. An arrow captures the enclosing module scope's `this` instead — usually `undefined` — and the whole thing silently fails. Type it as `function (this: Timer)` or `function (this: any)` so the checker keeps you honest. **2. It runs before field initializers.** For a non-static member the callback fires at the *beginning* of the constructor, ahead of the class's own field initializers. In the example above, `count = 0` has not run yet when the binding happens — harmless there, but fatal if a callback tries to read a field: ```typescript context.addInitializer(function (this: Timer) { console.log(this.count); // undefined — count = 0 has not run yet }); ``` If you need post-construction state, arrange for the work to happen later — from a field initializer transform on a field declared after the ones you need, or from an explicit init method. **3. `context.name` may be a symbol, and the member may be private.** `this[name]` works for a string or symbol key, but for a `#`-private method there is no key to index with. Use `context.access.get(this)` to read it, and understand that you cannot then install a shadowing own property under a private name — auto-binding a private method is not expressible this way. **4. Throwing aborts construction.** An initializer that validates and throws prevents the instance from escaping, which is a legitimate use, but it means an unexpected error in a decorator's initializer surfaces as a constructor failure far from the decorator's source. ## When to prefer the returned replacement instead Use the return value when the behaviour is genuinely per-class and stateless: logging, timing, memoizing on a key derived from arguments, argument coercion. Use `addInitializer` when the behaviour needs the instance: binding, per-instance caches, registering the instance with an observer, seeding per-instance state through `context.access.set`. Many real decorators do both — return a wrapper *and* register an initializer. ## The cost of auto-binding Binding per instance allocates one function object per decorated method per instance, and moves the method off the prototype for that object, so the shared version is no longer what `obj.tick` resolves to. In a class instantiated in the thousands that is measurable. The alternative — declaring the member as an arrow-function field, or binding explicitly at the point where the method is detached — costs nothing at the decorator level and is often the better default, with the decorator reserved for the cases where call sites cannot be changed.
- An initializer registered from a method decorator logs `undefined` when it reads a field that has an initializer. Why?Because a non-static member's initializer callback runs at the beginning of the constructor, before the class's field initializers execute. The field exists but has not been assigned yet. Move work that depends on field values out of the initializer — into a field transform on a later-declared field, or an explicit method called after construction.
- How does addInitializer behave differently when it is called from a class decorator?It runs once, after the class is fully defined rather than during construction, with `this` bound to the class itself. That makes it the right place for registration-style work that needs the finished constructor, and it is unrelated to instances — creating an instance later does not re-run it.
- Why is an auto-binding decorator often the wrong default in a hot class?Each decorated method costs one extra bound function object per instance, and the own property shadows the shared prototype method, so instances stop sharing that function. In a class constructed in bulk that is real memory and allocation pressure. Bind at the detachment site, or declare an arrow-function field only where a detached reference is actually taken.
saying these in an interview costs you the question
- Thinks a returned replacement can be bound per instance
- Writes the initializer as an arrow function
- Expects field values to be available inside the initializer
- Believes addInitializer runs once per class for instance members
- Ignores the per-instance allocation cost of auto-binding