TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
193: Class Decorators
Class decorators are one of those features that feel like magic until you realize they're just functions that wrap a constructor. I've always found them most useful when I need to apply "cross-cutting concerns"—things like logging, validation, or in our case today, immutability—without cluttering the actual business logic of the class.
The goal: An immutable configuration class
Imagine we're building a system where certain configuration classes should be strictly read-only once they're instantiated. Instead of manually adding readonly to every single property in every class (which is tedious and easy to forget), I want to create a @Frozen decorator. This decorator should ensure that any instance created from the decorated class cannot be modified at runtime.
function Frozen(constructor: Function) {
Object.freeze(constructor);
return constructor;
}
@Frozen
class AppConfig {
port = 8080;
env = 'development';
}
const config = new AppConfig();
config.port = 9000; // I expect this to fail or be ignored
console.log(config.port); // But will it?
Where I tripped up: Prototype vs. Instance
If you ran the code above, you'd notice something frustrating: config.port actually changed to 9000. I made this exact mistake when I first started using class decorators. I forgot that Object.freeze(constructor) freezes the class definition (the prototype and the constructor function itself), not the instances created by that class.
Freezing the constructor prevents me from adding new methods to AppConfig.prototype at runtime, but it does absolutely nothing to stop someone from changing config.port on a live object. To fix this, I need the decorator to return a new class that extends the original and freezes this inside its own constructor.
Implementing the instance freeze
To actually lock down the instances, I have to return a class expression. This is where the power of class decorators really kicks in. I can intercept the instantiation process and inject the freeze logic.
function Frozen(constructor: Function) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
Object.freeze(this);
}
};
}
@Frozen
class AppConfig {
port = 8080;
env = 'development';
}
const config = new AppConfig();
// In strict mode, this will now throw a TypeError
// In non-strict mode, it will simply be ignored
config.port = 9000;
console.log(config.port); // 8080 - Success!
Now, every time new AppConfig() is called, the wrapper class created by the decorator runs, calls the original constructor via super(), and then immediately locks the instance. I love this pattern because the AppConfig class remains clean and focused on its data, while the "immutability" behavior is handled externally by the decorator.
One thing to keep in mind: since we're returning a new class that extends the original, you're technically creating a subclass. For 99% of use cases, this is invisible and irrelevant, but it's good to be aware of if you're doing heavy instanceof checks or complex prototype manipulation.
📋 Practical Task
Building a Reportable Metadata Decorator
Your task is to create a class decorator called @Reportable. This decorator should add a new method to the class called report().
The report() method should:
- Log a string to the console in the format:
"Report for [ClassName]: [JSON string of all properties]". - Work regardless of what properties are defined in the class.
Requirements:
- Define the
Reportabledecorator function. - Apply it to a class named
UserSessionwith properties likeuserIdandloginTime. - Instantiate
UserSessionand call thereport()method to verify it prints the class name and the current state of the object.
There are no comments for now.