Decorators & Utility Types
Apply metadata and behavior modifications using decorators, and leverage built-in utility types for type transformations.
Decorators & Utility Types in TypeScript
Master TypeScript's advanced metaprogramming features with free flashcards and spaced repetition practice. This lesson covers decorators for runtime behavior modification, utility types for type transformations, and reflection metadataβessential concepts for building sophisticated TypeScript applications with clean, maintainable code.
Welcome to Advanced TypeScript π»
Welcome to one of TypeScript's most powerful feature sets! Decorators allow you to modify class behavior at design time, while utility types provide elegant ways to transform existing types. Together, these features enable you to write more expressive, maintainable code.
π― What You'll Learn:
- How decorators work and when to use them
- Built-in utility types for type manipulation
- Creating custom utility types
- Reflection and metadata APIs
- Real-world patterns and best practices
π‘ Prerequisites: You should be comfortable with TypeScript basics, including classes, interfaces, generics, and type inference.
Core Concept 1: Understanding Decorators π¨
Decorators are special declarations attached to classes, methods, properties, or parameters that modify their behavior. Think of them as "annotations" that add functionality without changing the original code.
How Decorators Work
A decorator is simply a function that receives information about the decorated element and can modify it:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
Decorator Syntax
| Decorator Type | Syntax | Use Case |
|---|---|---|
| Class | @decorator | Modify or seal classes |
| Method | @decorator | Intercept method calls |
| Property | @decorator | Initialize or validate properties |
| Parameter | @decorator | Mark parameters for injection |
| Accessor | @decorator | Modify getters/setters |
β οΈ Important: Enable "experimentalDecorators": true in your tsconfig.json to use decorators!
Decorator Execution Order
Decorators are applied in a specific order:
ββββββββββββββββββββββββββββββββββββββββββ β DECORATOR EXECUTION ORDER β ββββββββββββββββββββββββββββββββββββββββββ€ β β β 1οΈβ£ Instance Members (top to bottom) β β β’ Parameter decorators β β β’ Method/Accessor/Property β β β β 2οΈβ£ Static Members (top to bottom) β β β’ Parameter decorators β β β’ Method/Accessor/Property β β β β 3οΈβ£ Constructor Parameters β β β β 4οΈβ£ Class Decorator (outermost last) β β β ββββββββββββββββββββββββββββββββββββββββββ
Method Decorators
Method decorators receive three parameters:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
π‘ Tip: Method decorators are perfect for logging, performance monitoring, and validation!
Core Concept 2: Property & Class Decorators ποΈ
Property Decorators
Property decorators don't receive a property descriptor but can observe that a property has been declared:
function readonly(target: any, propertyKey: string) {
const descriptor: PropertyDescriptor = {
writable: false,
configurable: false
};
Object.defineProperty(target, propertyKey, descriptor);
}
class Person {
@readonly
id: number = 12345;
}
Class Decorators with Factories
Decorator factories let you customize decorator behavior:
function Component(config: { selector: string }) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
selector = config.selector;
render() {
console.log(`Rendering component: ${this.selector}`);
}
};
};
}
@Component({ selector: 'app-user' })
class UserComponent {
name = 'John';
}
π§ Try this: The factory pattern above is exactly how Angular's @Component decorator works!
Parameter Decorators
Parameter decorators mark parameters for special handling:
function inject(target: any, propertyKey: string | symbol, parameterIndex: number) {
const existingParameters = Reflect.getMetadata('inject', target, propertyKey) || [];
existingParameters.push(parameterIndex);
Reflect.defineMetadata('inject', existingParameters, target, propertyKey);
}
class UserService {
constructor(@inject private db: Database) {}
}
Core Concept 3: Built-in Utility Types π§
TypeScript provides powerful utility types that transform existing types. These are built into the language and don't require imports.
Partial
Makes all properties optional:
interface User {
id: number;
name: string;
email: string;
}
// All properties become optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
function updateUser(id: number, updates: Partial<User>) {
// Can update just name, just email, or both
}
Required
Makes all properties required (opposite of Partial):
interface Config {
host?: string;
port?: number;
ssl?: boolean;
}
type RequiredConfig = Required<Config>;
// { host: string; port: number; ssl: boolean; }
Readonly
Makes all properties readonly:
interface Point {
x: number;
y: number;
}
const point: Readonly<Point> = { x: 10, y: 20 };
// point.x = 5; // Error: Cannot assign to 'x' because it is a read-only property
Pick<T, K>
Selects specific properties from a type:
interface Article {
id: number;
title: string;
content: string;
author: string;
createdAt: Date;
}
type ArticlePreview = Pick<Article, 'id' | 'title' | 'author'>;
// { id: number; title: string; author: string; }
Omit<T, K>
Removes specific properties from a type:
type ArticleWithoutContent = Omit<Article, 'content'>;
// { id: number; title: string; author: string; createdAt: Date; }
π Common Utility Types Quick Reference
| Partial<T> | All properties optional |
| Required<T> | All properties required |
| Readonly<T> | All properties readonly |
| Pick<T, K> | Select specific properties |
| Omit<T, K> | Remove specific properties |
| Record<K, T> | Create object type with keys K and values T |
Core Concept 4: Advanced Utility Types π
Record<K, T>
Creates an object type with keys of type K and values of type T:
type Role = 'admin' | 'user' | 'guest';
type Permissions = Record<Role, string[]>;
const permissions: Permissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};
Exclude<T, U> & Extract<T, U>
Filter union types:
type Status = 'pending' | 'active' | 'suspended' | 'deleted';
// Remove types
type ActiveStatus = Exclude<Status, 'suspended' | 'deleted'>;
// 'pending' | 'active'
// Keep only specific types
type InactiveStatus = Extract<Status, 'suspended' | 'deleted'>;
// 'suspended' | 'deleted'
NonNullable
Removes null and undefined:
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
ReturnType & Parameters
Extract function signature information:
function createUser(name: string, age: number) {
return { name, age, id: Math.random() };
}
type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number; }
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]
Awaited
Unwraps Promise types:
type Response = Promise<{ data: string }>;
type UnwrappedResponse = Awaited<Response>;
// { data: string }
// Works with nested Promises
type Nested = Promise<Promise<number>>;
type Unwrapped = Awaited<Nested>;
// number
Example 1: Creating a Validation Decorator π‘οΈ
Let's build a practical decorator for property validation:
function MinLength(min: number) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = () => value;
const setter = (newVal: string) => {
if (newVal.length < min) {
throw new Error(`${propertyKey} must be at least ${min} characters`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class User {
@MinLength(3)
username!: string;
@MinLength(8)
password!: string;
}
const user = new User();
user.username = "ab"; // Error: username must be at least 3 characters
user.username = "alice"; // OK
Why this works:
MinLengthis a decorator factory that accepts configuration- It replaces the property with a getter/setter pair
- The setter validates the value before assignment
- Validation logic is reusable across properties
π‘ Real-world use: Libraries like class-validator use this pattern extensively!
Example 2: Method Timing Decorator β±οΈ
A decorator to measure method execution time:
function Timing(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
return result;
} finally {
const end = performance.now();
console.log(`${propertyKey} took ${(end - start).toFixed(2)}ms`);
}
};
return descriptor;
}
class DataService {
@Timing
async fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
return response.json();
}
@Timing
async processLargeDataset(data: number[]): Promise<number> {
return data.reduce((sum, num) => sum + num, 0);
}
}
// Output when methods are called:
// fetchUsers took 234.56ms
// processLargeDataset took 12.34ms
Key features:
- Works with async methods using
await - Uses
finallyto ensure timing happens even if method throws - Preserves the original method's return value
- Non-invasiveβno changes to method code
Example 3: Building Custom Utility Types π¨
Create your own utility types for common patterns:
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
// Make only 'description' optional
type ProductInput = PartialBy<Product, 'description'>;
// { id: number; name: string; price: number; description?: string; }
// Make nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
const partialConfig: DeepPartial<Config> = {
server: {
ssl: {
enabled: true
// 'cert' is optional due to DeepPartial
}
}
};
// Create a type-safe builder pattern
type Builder<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => Builder<T>;
} & { build(): T };
type UserBuilder = Builder<User>;
// {
// setId: (value: number) => UserBuilder;
// setName: (value: string) => UserBuilder;
// setEmail: (value: string) => UserBuilder;
// build(): User;
// }
π§ Try this: Combine multiple utility types to create complex transformations!
Example 4: Decorator Composition & Metadata π
Combine multiple decorators with reflection metadata:
import 'reflect-metadata';
// Route decorator
function Route(path: string) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('route:path', path, target, propertyKey);
};
}
// HTTP method decorator
function HttpGet(target: any, propertyKey: string) {
Reflect.defineMetadata('route:method', 'GET', target, propertyKey);
}
function HttpPost(target: any, propertyKey: string) {
Reflect.defineMetadata('route:method', 'POST', target, propertyKey);
}
// Validate decorator
function Validate(schema: any) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// Validation logic here
console.log('Validating with schema:', schema);
return originalMethod.apply(this, args);
};
};
}
class UserController {
@Route('/users')
@HttpGet
getUsers() {
return [{ id: 1, name: 'Alice' }];
}
@Route('/users')
@HttpPost
@Validate({ name: 'string', email: 'string' })
createUser(data: any) {
return { id: 2, ...data };
}
}
// Extract metadata for routing
function getRouteInfo(controller: any, methodName: string) {
const path = Reflect.getMetadata('route:path', controller.prototype, methodName);
const method = Reflect.getMetadata('route:method', controller.prototype, methodName);
return { path, method };
}
console.log(getRouteInfo(UserController, 'getUsers'));
// { path: '/users', method: 'GET' }
Pattern breakdown:
- Multiple decorators stack on a single method
reflect-metadatastores decorator information- Metadata can be read later for framework setup
- This pattern powers NestJS and other frameworks
Common Mistakes & How to Avoid Them β οΈ
Mistake 1: Forgetting Decorator Configuration
β Wrong:
// Decorators won't work!
class MyClass {
@log
method() {}
}
β Correct:
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Mistake 2: Losing this Context
β Wrong:
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
// 'this' context lost!
return original(args);
};
}
β Correct:
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
// Preserve 'this' with apply
return original.apply(this, args);
};
}
Mistake 3: Overusing Utility Types
β Wrong:
// Too complex, hard to read
type ComplexType = Partial<Pick<Omit<Required<User>, 'id'>, 'name' | 'email'>>;
β Correct:
// Break into steps
type RequiredUser = Required<User>;
type WithoutId = Omit<RequiredUser, 'id'>;
type NameAndEmail = Pick<WithoutId, 'name' | 'email'>;
type FinalType = Partial<NameAndEmail>;
// Or create a custom utility
type OptionalNameEmail = {
name?: string;
email?: string;
};
Mistake 4: Mutating Descriptors Incorrectly
β Wrong:
function readonly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
// Missing return!
}
β Correct:
function readonly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
return descriptor; // Always return
}
Mistake 5: Wrong Utility Type Choice
β Wrong:
// Using Partial when you need specific optional properties
type UpdateUser = Partial<User>;
// All properties become optional, even 'id'
β Correct:
// Use Omit + Partial for precise control
type UpdateUser = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;
// Only user data is optional, 'id' stays required
π§ Memory aid: "DAR" - Define, Apply, Return - the three steps of every decorator!
Key Takeaways π―
π Decorators & Utility Types Cheat Sheet
Decorators:
- Enable with
"experimentalDecorators": true - Execute order: instance β static β constructor β class
- Always preserve
thiscontext withapply() - Use factories for configurable decorators
- Great for: logging, validation, metadata, AOP
Essential Utility Types:
| Type | Purpose | Example |
|---|---|---|
Partial<T> | Optional properties | Update operations |
Required<T> | Required properties | Strict validation |
Readonly<T> | Immutable objects | Config objects |
Pick<T,K> | Select properties | API responses |
Omit<T,K> | Remove properties | Form inputs |
Record<K,T> | Map-like objects | Dictionaries |
ReturnType<T> | Function returns | Type inference |
Best Practices:
- Keep decorators focused and composable
- Combine utility types for complex transformations
- Document custom utility types clearly
- Test decorator behavior thoroughly
- Use metadata for framework-level features
π Further Study
Official Documentation:
- TypeScript Decorators - Complete decorator reference
- TypeScript Utility Types - All built-in utility types
Advanced Resources:
- Reflect Metadata - Metadata API for decorators
π Congratulations! You now have the tools to write more elegant, maintainable TypeScript code using decorators and utility types. Practice combining these features to build powerful abstractions!
π‘ Next steps: Explore how frameworks like NestJS and TypeORM use these patterns, then try building your own decorator-based library!