Method Decorators

This is part 1 of a series of articles about TypeScript decorators. Keep an eye out for additional articles!

One possible target for a TypeScript decorator is a method.  The API of a method decorator is as follows

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
  • target will hold a reference to the prototype of the class an instance method was defined on (or the constructor function in the case of a static method)
  • propertyKey will be the method name
  • descriptor is the property descriptor, which holds the implementation of the original method on its value property

You can either mutate the property descriptor, or return a new property descriptor to be used in its place.

Let's see it in action!

One use case I had in my own code base was as follows. Every aggregate root (Domain Driven Design) in our system is represented by a class with a validateInvariants(): Error | Valid method. Every time we update an aggregate root, we want to assert that the invariants are still satisfied. It is tedious and error prone to do this manually in every update method. Consider the following code.

class AudioItem extends AggregateRoot{
    // ...
    addParticipantToTranscript(participantInitials,participantName){
        this.participants.push(new TranscriptParticipant(participantInitials,participantName));
        
        const invariantValidationResult = this.validateInvariants();
        
        // this is repetetive
        if(invariantValidationResult instanceof Error){
            return invariantValidationResult;
        }
        
        return this;
    }
}

Wouldn't it be nice to simply write a wrapper to do the invariant validation for us? To achieve this, we will create a method decorator factory fucntion, i.e., a function that returns a method decorator.

function UpdateMethod(): MethodDecorator {
    return (_target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    // store a reference to the original implementation, which we will overwrite
        const originalImplementation = descriptor.value;

// here we wrap the original implementation
        descriptor.value = function (...args) {
            if (!(this instanceof AggregateRoot)) {
                throw new InternalError(
                    `A method must belong to an AggregateRoot class in order to be annotated as an update method`
                );
            }

            const updated = originalImplementation.apply(this, args) as AggregateRoot;

            if (!updated) {
                throw new Error(
                    `There is a problem with the implementation of: ${JSON.stringify(
                        propertyKey
                    )}. Did you remember to "return this;"?`
                );
            }

            const invariantValidationResult = updated.validateInvariants();

            if (isInternalError(invariantValidationResult)) {
                return invariantValidationResult;
            }

// This assumes that we are using a "chainable" API for update methods
            return updated;
        };
    };
}

Other use cases

Once upon a time, we followed a "clone on write" convention. I have since realized that this was a bit misguided, and have backed off of this in favour of explicit cloning when shared references would cause trouble (e.g., when building test data from a common starting point). Supposing that you wanted to clone on update, you could easily wrap this into the logic above. And if you did it this way, it would be super easy to get your performance back when you change your mind!

Another possible use case might be applying custom transformations to the return value of a method. For example, if you are writing Controllers \ HTTP Adapters for a REST (-ish) API, you might want to hijack the implementation and map instances of returned errors to a 400, and other values to a 200 (presumably having an outer try \ catch to filter thrown errors and map to 500). Or maybe you want to dynamically register your method as the logic for handling a request with a @Get("/heroes") decorator. If you use a framework like NestJs, this has already been written for you, but at least you understand what's going on behind the curtain now!