Introduction
TypeScript is a powerful superset of JavaScript that brings static typing and improved tooling to the JavaScript ecosystem. One of TypeScript's most intriguing features is decorators. Decorators are a form of meta programming that allows you to add and modify behavior to classes, methods, properties, and parameters. They provide a way to easily extend and modify the behavior of your code, making it more readable, maintainable, and expressive. In this article, we will delve into the world of TypeScript decorators, exploring their syntax, use cases, and practical applications.
Before start sure you add this configs to ts.config
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
Understanding TypeScript Decorators
Decorators are a way to add metadata to your TypeScript code. They are a form of higher-order function that can be applied to classes, methods, properties, and even method parameters. In essence, decorators allow you to wrap or modify the behavior of these code elements. This can be particularly useful for things like logging, validation, and dependency injection.
To use a decorator, you simply prefix the target element (class, method, property, or parameter) with the "@" symbol followed by the decorator name
function simpleDecorator() {
console.log('---hi I am a decorator---')
}
@simpleDecorator
class A {}
Exmaple
class User {
private static userType: string = "guest";
private email: string;
public username: string;
public addressLine1: string = "";
public addressLine2: string = "";
public country: string = "";
constructor(username: string, email: string) {
this.username = username;
this.email = email;
}
get userType() {
return User.gen;
}
get email() {
return this.email;
}
set email(newEmail: string) {
this.email = newEmail;
}
address(): any {
return `${this.addressLine1}\n${this.addressLine2}\n${this.country}`;
}
}
const user = new User("karimAdel88", "karimail88@yahoo.com");
user.addressLine1 = "Cairo";
user.addressLine2 = "Nasr City";
@frozen
decorator can be used to freeze a class in TypeScript. The@frozen
decorator is applied to the class constructor function, which then usesObject.freeze
to prevent any further modifications to both the constructor function and its prototype.
Let's break down the provided code step by step:
function frozen(target: Function) {
Object.freeze(target);
Object.freeze(target.prototype);
}
Here, thefrozen
decorator is defined. It takes thetarget
parameter, which is the constructor function of the class it is applied to.Object.freeze(target)
is used to freeze the class constructor itself, andObject.freeze(target.prototype)
is used to freeze the prototype of the class.
@frozen
class User {
constructor(name: string, email: string) {
// Constructor logic
}
}
In this code, the@frozen
decorator is applied to theUser
class, which effectively freezes the class constructor and its prototype.
console.log(Object.isFrozen(User)); // true
This line checks if theUser
class constructor is frozen, and it correctly returnstrue
, indicating that the class is indeed frozen.
User.addNewProp = "Trying to add new prop value"; // [ERR]: Cannot add property addNewProp, object is not extensible
In this part of the code, you attempt to add a new static propertyaddNewProp
to theUser
class, but it results in an error. This error occurs because theUser
class has been frozen by the@frozen
decorator, and frozen objects cannot be extended or modified. This behavior ensures that the class remains unaltered after applying the decorator.
console.log(Object.isFrozen(new User("example", "example@example.com"))); // false
In this line, you create a new instance of theUser
class, and then you check if the instance is frozen. The instance is not frozen, which is expected. The@frozen
decorator applied to the class targets the class constructor and its prototype, not instances of the class. Therefore, instances of theUser
class are not frozen, and you can still work with them as usual.
This example demonstrates how class decorators can be used to control and restrict modifications to classes in TypeScript, ensuring the immutability and stability of the class definition itself while allowing instances to be created and used without restrictions.
Property decorators in TypeScript allow you to modify the behavior of class properties. Let's start with a practical example. Consider aUser
class with two properties:username
andemail
. We want to ensure that these properties are required, meaning they must be initialized when a newUser
instance is created. To achieve this, we can define a@required
decorator.
function required(target: any, key: string) {
let currentValue = target[key];
Object.defineProperty(target, key, {
set: (newValue: string) => {
if (!newValue) {
throw new Error(`${key} is required.`);
}
currentValue = newValue;
},
get: () => currentValue,
});
}
The@required
decorator uses theObject.defineProperty
method to redefine the property's setter and getter. If an attempt is made to set the property to a falsy value (e.g., an empty string), it throws an error indicating that the property is required.
Now, when you create aUser
instance without providing values forusername
andemail
, the decorator will enforce the requirement:
const p = new User("", "example@example.com"); // [ERR]: username is required.
const u = new User("example", ""); // [ERR]: email is required.
This demonstrates how property decorators can be used to add custom validation rules and behaviors to class properties.
Property decorators not only work with regular properties but also with property accessors, including getter and setter methods. When applying a decorator to a property accessor, you have access to the property descriptor, in addition to the target and key.
Consider a scenario where we want to control theenumerable
attribute of a property accessor using a@enumerable
decorator:
function enumerable(isEnumerable: boolean) {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
descriptor.enumerable = isEnumerable;
console.log(
"The enumerable property of this member is set to: " +
descriptor.enumerable
);
};
}
In this example, the@enumerable
decorator sets theenumerable
attribute of the property descriptor based on the provided boolean value. This allows us to control whether the property accessor is enumerable or not.
Here's how you can apply the@enumerable
decorator to a property accessor:
class User {
@enumerable(false)
get userType(): string {
return "standard";
}
}
When you create an instance of theUser
class, the console will print a message indicating the enumerable status of theuserType
property accessor.
The@enumerable
decorator demonstrates the concept of a decorator factory. Decorator factories are functions that produce decorators with customizable behavior. In this case, theenumerable
decorator factory takes a boolean input, allowing you to control whether a property accessor is enumerable or not.
Decorator factories are commonly used to create reusable and parameterized decorators, making it easy to tailor the behavior of decorators to specific use cases.
While property decorators modify properties, method decorators work with methods. A common use case for method decorators is to mark methods as deprecated while still allowing them to be used. Let's take a look at a@deprecated
method decorator:
function deprecated(target: any, key: string, descriptor: PropertyDescriptor) {
const originalDef = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Warning: ${key}() is deprecated. Use other methods instead.`);
return originalDef.apply(this, args);
};
return descriptor;
}
The@deprecated
decorator logs a warning message to the console and calls the original method when the decorated method is invoked. This allows you to inform users that a method is deprecated while still providing backward compatibility.
Here's how you can apply the@deprecated
decorator to a method:
class User {
@deprecated
address() {
// Method implementation
}
}
When you call theaddress
method, you'll see the deprecation warning message in the console.
Thanks for reading.