🎨 TypeScript Decorators: Magic Stickers for Your Code
Imagine you have a plain notebook. Now imagine you can stick magical stickers on it that give it superpowers—like making it glow, talk, or remember things. That’s exactly what decorators do in TypeScript!
🌟 What Are Decorators? (The Big Picture)
Think of decorators like special labels you stick on things to give them new abilities.
Real Life Example:
- You have a plain white t-shirt 👕
- You stick a “Glow in the Dark” label on it ✨
- Now the t-shirt glows at night!
In TypeScript, decorators are those magical labels. You stick them on:
- Classes (the whole t-shirt)
- Methods (the sleeves)
- Properties (the buttons)
- Parameters (the thread)
@GlowInTheDark // ← This is a decorator!
class TShirt {
// Now TShirt has superpowers!
}
The @ symbol is like saying “Hey, stick this label here!”
🏠 Decorators Overview
How Do They Work?
A decorator is just a function that receives information about what it’s decorating.
graph TD A["You write @something"] --> B["TypeScript sees the @"] B --> C["Calls your decorator function"] C --> D["Your function adds magic!"]
The Golden Rule
Decorators run when your code loads, NOT when you use the class!
Think of it like:
- You put stickers on toys at the factory 🏭
- When someone buys the toy, stickers are already there
- You don’t add stickers when playing with the toy
Enabling Decorators
Before using decorators, tell TypeScript “I want to use magic stickers!”
In your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
🎪 Class Decorators: Decorating the Whole Thing
A class decorator is like wrapping a gift box. You take the whole box and add something to it.
Simple Example
function Frozen(target: Function) {
// Make the class unchangeable
Object.freeze(target);
console.log(`${target.name} is frozen!`);
}
@Frozen
class IceCream {
flavor = "vanilla";
}
// Output: "IceCream is frozen!"
What happened?
- We created a decorator called
Frozen - We stuck it on
IceCreamclass - Now nobody can add new things to
IceCream!
Real-World Example: Adding Features
function WithTimestamp(target: Function) {
target.prototype.createdAt = new Date();
target.prototype.showAge = function() {
console.log(`Born on: ${this.createdAt}`);
};
}
@WithTimestamp
class Document {
title = "My Doc";
}
const doc = new Document();
(doc as any).showAge();
// Output: "Born on: [current date]"
The Magic: We added createdAt and showAge() to EVERY Document, without writing them inside the class!
🔧 Method Decorators: Superpowers for Functions
Method decorators let you wrap or modify what a function does.
The Three Gifts
When you decorate a method, TypeScript gives you three things:
| Gift | What It Is | Like… |
|---|---|---|
target |
The class | The whole toy box |
propertyKey |
Method name | Label on the toy |
descriptor |
Method details | Instructions sheet |
Simple Example: Logging
function LogIt(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey}...`);
const result = original.apply(this, args);
console.log(`Done! Result: ${result}`);
return result;
};
}
class Calculator {
@LogIt
add(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Output:
// "Calling add..."
// "Done! Result: 5"
What happened?
We wrapped the add method with extra code that runs before and after!
Another Example: Timing
function Timer(
target: any,
key: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = Date.now();
const result = original.apply(this, args);
const end = Date.now();
console.log(`${key} took ${end - start}ms`);
return result;
};
}
class DataProcessor {
@Timer
processData() {
// Some heavy work...
for(let i = 0; i < 1000000; i++) {}
return "done";
}
}
📦 Property Decorators: Labels for Your Stuff
Property decorators help you watch or control class properties.
What You Get
| Gift | What It Is |
|---|---|
target |
The class prototype |
propertyKey |
Name of the property |
Simple Example: Required Fields
function Required(target: any, propertyKey: string) {
let value: any;
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue) {
if (newValue === undefined || newValue === null) {
throw new Error(`${propertyKey} is required!`);
}
value = newValue;
}
});
}
class User {
@Required
name!: string;
@Required
email!: string;
}
const user = new User();
user.name = "Alice"; // ✅ Works fine
user.email = null; // ❌ Error: email is required!
Another Example: Auto-Format
function Uppercase(target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: string) {
value = newValue.toUpperCase();
}
});
}
class Message {
@Uppercase
text!: string;
}
const msg = new Message();
msg.text = "hello world";
console.log(msg.text); // "HELLO WORLD"
🎯 Parameter Decorators: Watching the Inputs
Parameter decorators let you mark and track function inputs.
What You Get
| Gift | What It Is |
|---|---|
target |
The class prototype |
propertyKey |
Method name |
parameterIndex |
Position of the parameter (0, 1, 2…) |
Simple Example: Marking Important Params
const requiredParams: Map<string, number[]> = new Map();
function Important(
target: any,
methodName: string,
paramIndex: number
) {
const existing = requiredParams.get(methodName) || [];
existing.push(paramIndex);
requiredParams.set(methodName, existing);
console.log(
`Parameter ${paramIndex} of ${methodName} is important!`
);
}
class OrderService {
placeOrder(
@Important customerId: string,
@Important productId: string,
quantity: number
) {
return `Order placed!`;
}
}
// Output:
// "Parameter 0 of placeOrder is important!"
// "Parameter 1 of placeOrder is important!"
Combining with Method Decorators
Parameter decorators collect info. Method decorators use that info!
const validatedParams: number[] = [];
function Validate(
target: any,
methodName: string,
paramIndex: number
) {
validatedParams.push(paramIndex);
}
function ValidateAll(
target: any,
methodName: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
for (const index of validatedParams) {
if (args[index] === undefined) {
throw new Error(`Argument ${index} is missing!`);
}
}
return original.apply(this, args);
};
}
class API {
@ValidateAll
fetchUser(@Validate id: string) {
return { id, name: "User" };
}
}
🎭 Execution Order: Who Goes First?
When you have multiple decorators, they run in a specific order:
graph TD A["1. Parameter Decorators"] --> B["2. Method Decorators"] B --> C["3. Property Decorators"] C --> D["4. Class Decorators"]
Memory Trick: “Params → Methods → Props → Class” Think: People Make Pizza Crusts
Multiple Decorators on Same Thing
They run bottom to top:
@First // Runs SECOND
@Second // Runs FIRST
class Example {}
It’s like putting on clothes:
- Put on shirt first (bottom decorator)
- Then jacket (top decorator)
🎁 Quick Summary
| Decorator Type | Decorates | Use Case |
|---|---|---|
| Class | Whole class | Add features, freeze, log |
| Method | Functions | Time, log, validate |
| Property | Variables | Auto-format, validate |
| Parameter | Inputs | Mark, track, validate |
🚀 You Did It!
You now understand TypeScript decorators! They’re like magical stickers that give your code superpowers:
- Class decorators = Gift wrapping the whole present
- Method decorators = Adding a surprise mechanism
- Property decorators = Labeling what’s inside
- Parameter decorators = Checking the ingredients
Next time you see @something, you’ll know: “That’s just a magic sticker adding superpowers!” ✨
