I'm Samuel FajreldinesI am a specialist in the entire JavaScript and TypeScript ecosystem (including Node.js, React, Angular and Vue.js) I am expert in AI and in creating AI integrated solutions I am expert in DevOps and Serverless Architecture (AWS, Google Cloud and Azure) I am expert in PHP and its frameworks (such as Codeigniter and Laravel). |
Samuel FajreldinesI am a specialist in the entire JavaScript and TypeScript ecosystem. I am expert in AI and in creating AI integrated solutions. I am expert in DevOps and Serverless Architecture I am expert in PHP and its frameworks.
|
TypeScript decorators are a powerful feature that can transform the way code is organized, annotated, and maintained. By attaching special behavior to classes, methods, accessors, properties, or parameters, decorators dramatically improve readability and structure in large-scale applications. While decorators are widely used in frameworks like Angular, they can also be employed in other contexts, from Node.js backends to library development. This comprehensive guide explores how to use and create TypeScript decorators, offering tangible examples and best practices to enhance your development workflow.
In general programming terms, a decorator is a way to add additional functionality to code without modifying its original structure. In TypeScript, decorators work in a similar fashion by allowing you to apply annotation-based logic to your classes, methods, and more. Each decorator is essentially a function that receives some form of metadata about the target it is decorating (e.g., a class constructor, a method descriptor) and can perform operations like wrapping, modifying, or logging specific behavior.
Cleaner Code: Instead of stuffing business logic or cross-cutting concerns (such as logging or performance metrics) directly into your class methods, decorators let you annotate these elements with focused code. This approach offers a clear separation of responsibilities and allows code to remain more cohesive.
Reusability: Once a decorator is defined, it can be easily reused throughout an application or shared between multiple projects. For example, you can create a logging decorator and apply it to multiple classes to track performance metrics or log method calls consistently.
Enhanced Readability: Decorators convey intent. When you see a method named @Log()
or a class marked with @Controller()
, you instantly understand what additional capabilities or transformations are being applied. This leads to more maintainable codebases, especially when working in teams where multiple contributors need to grok the code quickly.
Framework Integration: Angular introduced a mainstream use of TypeScript decorators for denoting components, modules, services, and more. Recognizing their power and readability, other frameworks can take advantage of decorators as well, making them a valuable tool for any TypeScript developer aiming to build robust and scalable applications.
TypeScript provides different kinds of decorators, each targeting a specific language feature. Knowing which decorator to employ in each scenario is key to maximizing their benefits:
Class Decorators
A class decorator is a function that takes a constructor as its parameter. It can create new constructors, perform additional logic, or attach new properties to the class. They are commonly used to mark classes with specific roles (like @Controller()
) and to perform any high-level initialization or transformation.
Method Decorators
A method decorator receives a target (the class prototype), a method name, and a property descriptor. You can use method decorators to define logging, caching, or other cross-cutting concerns that should apply every time a specific method is called. For instance, you might intercept an HTTP call and automatically add headers or handle exceptions.
Accessor Decorators
Similar to method decorators, accessor decorators intercept property getters and setters. They are often used for data validation, transformations, or logging property changes. While not as frequently used as class or method decorators, they can be invaluable in certain data-centric scenarios.
Property Decorators
A property decorator receives a target and the name of the decorated property. Though they cannot directly modify property initializers, they can attach metadata to properties. This is especially useful for automatic ORM bindings or serializing class properties to JSON.
Parameter Decorators
Finally, parameter decorators allow you to annotate parameters within a method’s signature. They are often used in frameworks to inject services or dependencies, giving you the ability to peel away complexities from your method signatures.
To use decorators in TypeScript, enable the experimentalDecorators
compiler option in your tsconfig.json
:
{
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
...
},
...
}
experimentalDecorators
: Allows the usage of decorators.emitDecoratorMetadata
: Emits type metadata, particularly important for more advanced reflection scenarios (for instance, using the reflect-metadata
library).With these configurations set, you are ready to start applying decorators in your TypeScript projects.
Here is a simple class decorator that adds a timestamp property to track instantiation times:
function Timestamped(constructor: Function) {
constructor.prototype.createdAt = new Date();
}
@Timestamped
class ReportService {
constructor() {
// Some initialization code
}
generateReport() {
console.log("Report generated");
}
}
const service = new ReportService();
console.log(service["createdAt"]); // A Date object representing creation time
In this example:
Timestamped
is a class decorator function.constructor.prototype
to include a createdAt
property, set to the current time.ReportService
will now have a createdAt
metadata attached.Method decorators allow you to intercept and augment function calls. Here’s a simple logging decorator:
function LogMethod(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyName} with arguments:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result of ${propertyName}:`, result);
return result;
};
return descriptor;
}
class TaskService {
@LogMethod
runTask(taskId: string) {
return `Task ${taskId} completed successfully!`;
}
}
const taskService = new TaskService();
taskService.runTask("1234");
In the above code:
LogMethod
intercepts calls to runTask
.runTask
.This separation of concerns allows you to cleanly handle cross-cutting logic like logging without cluttering core methods.
Property decorators can attach metadata to specific class properties. For instance, imagine an ORM that needs to identify which properties map to database columns:
import "reflect-metadata";
const COLUMN_KEY = Symbol("column");
/**
* Attach metadata to a property to mark it as a DB column.
*/
function Column(columnName?: string): PropertyDecorator {
return (target, propertyKey) => {
Reflect.defineMetadata(COLUMN_KEY, columnName || propertyKey, target, propertyKey);
};
}
class User {
@Column("user_id")
id: number;
@Column()
username: string;
@Column()
email: string;
}
// Later you can retrieve metadata to build queries or define migrations
function getColumnMetadata(target: any, propertyKey: string) {
return Reflect.getMetadata(COLUMN_KEY, target, propertyKey);
}
With this pattern:
@Column()
is annotated with metadata specifying the corresponding column name.This approach highlights the real power of decorators for building flexible, annotation-driven systems.
Creating new decorators follows patterns similar to those above. You define a function that accepts certain parameters (depending on whether it decorates a class, method, etc.) and then returns a function that targets the code piece you want to augment.
Suppose you want a simple validation system that checks whether an argument passed into a method is not empty. Here’s how you can build a custom method parameter decorator to enforce that:
import "reflect-metadata";
const VALIDATE_KEY = Symbol("validateParams");
function NotEmpty(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
VALIDATE_KEY,
existingRequiredParameters,
target,
propertyKey
);
}
function Validate(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] =
Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyName) || [];
requiredParameters.forEach((index) => {
if (args[index] === null || args[index] === undefined || args[index] === "") {
throw new Error(`Validation Error: Parameter at index ${index} is empty`);
}
});
return method.apply(this, args);
};
return descriptor;
}
class MailService {
@Validate
sendEmail(@NotEmpty recipient: string, message: string) {
console.log(`Sending email to ${recipient} with message: ${message}`);
}
}
const mailService = new MailService();
mailService.sendEmail("john@example.com", "Hello John!"); // Works fine
mailService.sendEmail("", "This should fail validation"); // Throws Validation Error
What’s going on here?
@NotEmpty
is a parameter decorator that records the index of the parameter in the method’s signature.@Validate
is a method decorator that retrieves those recorded parameter indexes and checks whether they have valid values before executing the method.This modular approach underscores how decorators can elegantly implement advanced logic in a reusable way.
Angular relies heavily on TypeScript decorators to identify classes as components, directives, pipes, etc. When you see an @Component(...)
annotation, it’s effectively a class decorator that extends and annotates the class to be recognized by Angular’s compilation process. Similarly, services use the @Injectable()
decorator, which allows them to be injected into different parts of the application.
Despite this heavy usage being somewhat behind-the-scenes, understanding how decorators work under the hood helps in debugging, custom library development, or advanced framework modifications.
Keep Them Focused: Decorators should have a single responsibility, such as logging, validation, or transformation. Overloading a decorator with multiple tasks can lead to confusion and complexity.
Limit Shared State: When decorators modify class prototypes or method descriptors, ensure changes are contained and well-documented. Avoid patterns that might mask or overwrite existing functionality without transparency.
Leverage Metadata Wisely: Libraries like reflect-metadata
make it easy to store and retrieve annotated data. Use it judiciously to maintain clarity and performance. If excessive metadata is attached, it can become challenging to maintain or reason about.
Combine with Other Patterns: Decorators are an excellent complement to dependency injection, factory methods, or middleware-based designs. For example, you could write a decorator that automatically transforms method responses into HTTP responses, bridging the gap between your domain logic and the web framework.
Test Thoroughly: Because decorators often implement cross-cutting concerns, they can be easy to forget or overlook during testing. Ensure that your unit tests cover both the core functionality and the behavior introduced by decorators.
Armed with an understanding of decorators, you can begin applying them to real-world applications. Whether your goal is to streamline logging, enforce validation, or embed domain-specific metadata, decorators offer a clean, modular way to enhance your code. They can dramatically reduce boilerplate, promote consistency, and empower you to shape the behavior of your application in new and exciting ways.
Beyond these basics, there’s plenty more to explore. Advanced topics include:
By consistently employing well-crafted decorators, you can not only improve the maintainability of your applications but also offer new levels of clarity for your development team. Whether you’re architecting a massive enterprise platform or experimenting with a personal side project, harnessing the full potential of TypeScript decorators will help you craft more flexible, concise, and robust solutions.
About Me
Since I was a child, I've always wanted to be an inventor. As I grew up, I specialized in information systems, an area which I fell in love with and live around it. I am a full-stack developer and work a lot with devops, i.e., I'm a kind of "jack-of-all-trades" in IT. Wherever there is something cool or new, you'll find me exploring and learning... I am passionate about life, family, and sports. I believe that true happiness can only be achieved by balancing these pillars. I am always looking for new challenges and learning opportunities, and would love to connect with other technology professionals to explore possibilities for collaboration. If you are looking for a dedicated and committed full-stack developer with a passion for excellence, please feel free to contact me. It would be a pleasure to talk with you! |
SecurityScoreCard
Nov. 2023 - Present
New York, United States
Senior Software Engineer
I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.
I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.
Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.
Technologies Used:
Node.js Terraform React Typescript AWS Playwright and Cypress