Devot Logo
Devot Logo
Arrow leftBack to blogs

Exploring TypeScript Metaprogramming

Juraj S.5 min readSep 3, 2024Technology
Juraj S.5 min read
Contents:
What are descriptors?
What are decorators?
How descriptors and decorators work together
Constructor
Memoization
Callback functions
Sealing a Class
Conclusion

The definition of metaprogramming is really quite simple: programs that manipulate programs.

Therefore, parsers, compilers, interpreters, and domain-specific languages are actually meta programs. In general-purpose languages, we use metaprogramming for things like code generation and serializers and deserializers, but in this blog, I want to focus on aspect-oriented programming.

The idea for this blog came from a workshop I attended at Web Summer Camp, hosted by Daniel Ostrovsky. It got me really interested in metaprogramming in TypeScript, so I decided to dive into the topic.

Aspect-oriented programming (AOP) is a programming paradigm that specifically addresses cross-cutting concerns. These cross-cutting concerns are parts of a program that rely on or must affect many other parts of the system.

The most common such cases are logging, validation, error handling, and transaction management. AOP aims to separate these cross-cutting concerns from the core business logic.

In TypeScript, we can implement these aspects using method descriptors and decorators.

What are descriptors?

Descriptors in TypeScript are objects that provide metadata about properties, methods, and parameters of classes. This metadata allows developers to dynamically alter the behavior of these elements at runtime.

For instance, a descriptor object typically gives us information about whether a property is configurable (can be changed), enumerable (can be listed in a loop), writable (can be modified), and what its value is. For methods, descriptors can include details about getters and setters, enabling dynamic control over how properties are accessed and modified.

What are decorators?

A decorator is a special kind of declaration that can be attached to a class, method, accessor, property, or parameter.

Decorators are written in the form @expression, where the expression must evaluate to a function. This function is called at runtime and contains information about the element being decorated.

To create a decorator, you'll need to define a function that accepts parameters and manipulates the descriptor accordingly. The specific parameters will vary depending on what you intend to decorate.

The order in which you declare decorators matters because the composition of two functions is not commutative. This means that swapping two decorators can result in different behavior.

Before using decorators, ensure they are enabled in your tsconfig.json configuration file:

How descriptors and decorators work together

Logging

Suppose you are in the process of creating a new method and want to log what is happening in your application. You might add a couple of console logs here and there like this:

Unfortunately, this becomes a "mess" to read as console.log statements take up space. We can avoid this by creating a separate function for logging:

Now, we can just add this as a decorator to our function. This is called a function decorator:

We have achieved the same functionality without actually touching our original function, and therefore, our business logic is completely separate from our logging aspect.

Constructor

A fun way to use decorators is to expand a class by hijacking the constructor method and adding a new field to the class.

This way we extend the Monster class to also have a power attribute.

Memoization

Memoization is an optimization technique primarily used to enhance the performance of computer programs. It works by storing the results of expensive function calls and returning the cached result when the same inputs are encountered again.

The process involves wrapping the method so that before the function executes, the cache is checked to see if the result for the given parameters already exists. If the result isn’t cached, it’s computed and then stored for future use. Instead of using a simple object {} for caching results, it's recommended to use the Map object (see the Map documentation). The Map allows for any type of key, not just strings, making it particularly useful in this scenario.

Now just add @memoization to the multiplier method.

In the first call, the multiplier is invoked and it calculates the xp multiplier. However, during the second call, the result for multiplier is retrieved from the cache, thanks to memoization, so the function doesn't need to be computed again.

Callback functions

Callback functions are a powerful concept that allows us to pass a function as an argument to another function, enabling more dynamic and flexible code execution. This pattern is especially useful when you want to abstract away certain behaviors and make your code more modular.

Let's consider a scenario in a fantasy game where a Npc greets a player differently whether they are an Elf or a Gnome perhaps. We want this greeting behavior to be flexible, allowing different NPCs to greet the player differently. Well we can achieve this using a callback function in TypeScript:

This is a higher-order function (a function that takes another function as an argument), you can create more complex and versatile functionality. For instance, you might want to add additional processing to the greeting (e.g., logging, modifying the message, or handling special cases) inside npcGreeting without altering the greetPlayer method itself.

Sealing a Class

In this example, we will use a class decorator to seal the Monster class, preventing any modifications to its structure, such as adding new properties or methods. This is particularly useful in situations where you want to ensure that the class's definition remains unchanged.

Decorator definition

Using the decorator with the Monster Class

The sealed decorator calls Object.seal() on the Monster class constructor and its prototype. This ensures that no new properties or methods can be added to the Monster class after it has been sealed.

The Monster class still includes a health property and a takeDamage method that reduces the monster's health by a specified amount of damage. But after applying the sealed decorator, any attempt to add new methods or properties to the Monster class will fail. In this case, trying to add a newMethod to Monster.prototype will result in undefined.

This can be a powerful tool for ensuring the integrity of your classes in a library or application.

Conclusion

In TypeScript, decorators provide a powerful mechanism for encapsulating common behaviors and applying them consistently across your codebase. By allowing you to define and reuse functionality in a modular way, decorators help reduce code duplication, minimize potential bugs, and enhance code readability.

In this blog post, we explored some basic examples of decorators, such as memoization, logging, and sealing classes. These examples demonstrate how decorators can simplify your code and enforce consistency.

As you continue to explore decorators, you'll discover even more advanced use cases, such as input validation, execution time tracking, and beyond, enabling you to write cleaner, more maintainable TypeScript code.

Spread the word:
Keep readingSimilar blogs for further insights
How JavaScript Signals Are Changing Everyday Development
Technology
Hrvoje D.5 min readNov 7, 2024
How JavaScript Signals Are Changing Everyday DevelopmentSignals are getting popular lately, but why is that? Read the blog to discover how signals in JavaScript are transforming code to be more concise, readable, and understandable.
Building Our New Website with Next.js: The Benefits and Challenges
Technology
Mario F.Luka C.5 min readOct 18, 2024
Building Our New Website with Next.js: The Benefits and ChallengesWe decided to rewrite our website, focusing on design and maintainability. Read why we chose Next.js.
Optimize Web Applications With NextJS Server and Client Components
Technology
Luka C.5 min readOct 11, 2024
Optimize Web Applications With NextJS Server and Client ComponentsIn this blog post, explore how the dynamic duo of Next.js server and client components can improve your web applications.