When to create custom decorators in TypeScript
Decorators save you repetitive code and make your architecture declarative. Create one when you need to: apply the same logic to multiple classes/methods (logging, validation, cache), modify behavior without touching implementation, or add metadata for runtime reflection. Classic example: @Authorize('admin') is cleaner than checking roles in every endpoint. They're also useful for cross-cutting concerns (security, monitoring, transactions) that don't belong in business logic. If you're copying the same try-catch or timer in 10 methods, you need a decorator.
Types of decorators and their use cases
Class decorators: Modify constructors, useful for DI or registering classes (@Injectable, @Entity). Method decorators: Wrap methods, ideal for logging, cache, validation (@Log, @Cache). Property decorators: Modify properties, used for metadata or transformations (@Column, @Expose). Parameter decorators: Mark parameters, common in web frameworks (@Body, @Param). NestJS and Angular use all types; TypeORM focuses on class and property; in custom libraries, method decorators are most versatile.
How to name decorators for self-documentation
Use clear verbs or states: @Validate says what it does, @IsEmail what it verifies, @Authorized what it requires. Avoid generic names like @Do or @Handle. If the decorator receives parameters, the name should suggest what it expects: @Cache(60) (TTL in seconds), @Roles('admin', 'editor') (role strings). Common prefixes: Is- for boolean validations, Use- for applying middleware, Before/After for lifecycle hooks. Consistency within project: if you use @LogStart, don't use @TraceEnd for the pair; better @LogEnd.
Common mistakes when implementing decorators
Not preserving metadata: Some decorators overwrite existing metadata; use Reflect.defineMetadata with merge. Not binding this: If the decorator wraps a method, you lose class context; use arrow functions or .bind(this). Build time vs runtime execution: Decorators run when declaring the class, not when instantiating; for dynamic logic, return a function. Not typing parameters: @Custom(options: any) is a trap; define clear interfaces. Forgetting experimentalDecorators: In tsconfig.json it must be true. Not documenting behavior: A decorator without docstring is a black box.
Reference table: TypeScript utility types
Beyond decorators, TypeScript ships built-in utility types that transform existing types without rewriting them. The ones you'll use most, what they do and an example (assume a type type T = { id: number; name: string }).
| Type | What it does | Example |
|---|---|---|
Partial<T> | Makes all properties optional | Partial<T> → { id?: number; name?: string } |
Required<T> | Makes all properties required | Required<{ id?: number }> → { id: number } |
Readonly<T> | Makes all properties read-only | Readonly<T> → { readonly id: number; ... } |
Pick<T, K> | Keeps only keys K from T | Pick<T, 'id'> → { id: number } |
Omit<T, K> | Keeps T without keys K | Omit<T, 'id'> → { name: string } |
Record<K, V> | Builds an object type with keys K and values V | Record<string, number> → { [k: string]: number } |
Exclude<U, E> | Removes from union U the members assignable to E | Exclude<'a' | 'b', 'a'> → 'b' |
Extract<U, E> | Keeps in U only the members assignable to E | Extract<'a' | 'b', 'a'> → 'a' |
ReturnType<F> | Extracts a function's return type | ReturnType<() => number> → number |
Parameters<F> | Extracts a function's parameters as a tuple | Parameters<(a: string) => void> → [string] |
Awaited<T> | Unwraps the resolved type of a Promise | Awaited<Promise<string>> → string |