EnriqueMark
TS

Decorators

2026-04 English

The ts decorator is essentially still a higher-order function, and its use cases are about the same as py decorators: when I need to “wrap” some object but don’t want to rewrite the whole original method, I use a decorator. Like adding a log to an existing method, which is the classic @ case in py too.

function logger(target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value  // grab the original method
    
    descriptor.value = function(...args: any[]) {
        console.log(`calling ${key}`)
        return original.apply(this, args)
    }
    // no return needed, just change descriptor.value
}

class Greeter {
    @logger
    greet(name: string) {
        console.log(`hello ${name}`)
    }
}

The target, key, descriptor here is a ts thing, or really a js thing, essentially what the prototype mechanism needs. target points straight at the prototype chain of the method or the class; key is the easy one, it’s the name of the method; descriptor is the method’s “settings,” and we usually only care about descriptor.value, which is the method itself, the equivalent of py’s func here.

This is also the annoying part of ts. In py I can return a new function directly, the body of it is plain func(), and then I put whatever logic wrapping I need in front of it. But in ts I have to rewrite the method with a very annoying mechanism.

function logger(target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value  // save the original method

    descriptor.value = function(...args: any[]) {
        console.log(`calling ${key}, args:`, args)
        const result = original.apply(this, args)  // call the original method
        console.log(`${key} returned:`, result)
        return result
    }

    // no return needed, changing descriptor is enough to make it work
}

You can see from the whole logic above that understanding ts’s @ comes down to understanding js’s prototype chain mechanism. Every method hangs on the prototype chain, and to keep a method working you have to hand its whole “kit” back untouched. this and function, for example. The reason not to use an arrow function is that this gets lost here, and once it’s lost, all the this operations inside my original method are gone, which is the same even without a decorator.

this points at its own parent instance, which is self in py. Inside a class I access a property with self.xxx, and in js it’s this.xxx. All methods hang under their own parent prototype chain, and the properties on that parent prototype chain work the same way. this.xxx means the xxx under the parent instance, and js looks it up along the prototype chain by default. An arrow function loses this context, its this points at the outer object at definition time instead of the current calling object. In a wrapping case like this, if I use an arrow function it points at logger, and if the wrapped thing is a method under some class, it gets nothing at all from the properties of the class it belongs to, because this doesn’t point at it. So here I have to pick function, and this points straight at the context of the caller (the wrapped object), and then the this passed inside points at the right calling object.

What original.apply(this, args) does is pass the this context along, so that this ends up matching the wrapped object instead of getting lost because I rewrote it.


Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 装饰器.