What decorators are (and which “version” you’re using)
Decorators are a meta-programming feature: you attach @something to a class or a class member, and TypeScript calls your decorator function with metadata about that declaration. They’re commonly used for logging, auto-binding, dependency injection, and validation. (typescriptlang.org )
One important context point: the decorator signatures in your notes ((target, propertyName), (target, name, descriptor), parameter decorators, etc.) correspond to TypeScript’s legacy “experimental decorators” model (enabled via experimentalDecorators). (typescriptlang.org )
TypeScript 5.0+ also supports the newer ECMAScript decorators proposal, which is emitted and type-checked differently, and notably doesn’t allow parameter decorators. (typescriptlang.org )
Property decorators: run at class definition time
A property decorator runs when the class is defined (not when you instantiate it). It tells you:
target: the prototype for instance properties (or constructor for static properties)propertyName: the name of the decorated property
function Log(target:any, propertyName: string){
console.log(`Property decorator!`)
console.log(target,propertyName); // prototype of object, name of property: {constructor: f,getPriceWithTax:f},"title"
}
Product {
@Log
title: string
_price:number
set _price(val:number){
if(val>0){
this._price = val;
}else{
throw new Error('invalid price- should be positive');
}
}
constructor(t:string, p:number){
this.title = t;
this._price = p;
}
getPriceWithTax(tax:number){
return this._price*(1+tax);
}
}Key idea: property decorators are best for registering metadata (“this field is required”, “this field maps to column X”), because they don’t get a property descriptor (more on that below). (typescriptlang.org )
Practical gotcha in the snippet (conceptually): using
set _price(...) { this._price = val }would recurse forever in real code unless you use a different backing field name. The decorator concept still stands.
Accessor, method, and parameter decorators (what arguments they get)
Legacy TypeScript decorators support:
- Accessor decorators: for
get/set - Method decorators: for methods
- Parameter decorators: for method/constructor params
These receive richer context than property decorators.
function Log(target:any, propertyName: string){
console.log(`Property decorator!`)
console.log(target,propertyName);
}
function Log2(target:any, name: string, descriptor: PropertyDescriptor){// setter decorator
console.log(`Accessor decorator`)
console.log(`target, name, descriptor`)
}// prototype or constructor function if static, name of accessor, descriptor of property: {constructor: f, getPriceWithTax: f},"price",{get:undefined, enumerable: false, configurable: true, set: f}
function Log3(target:any, name: string|Symbol,descriptor: PropertyDescriptor ){//Method decorator
console.log(`Method decorator`);
console.log(target,name,descriptor)// {constructor: f, getPriceWithTax: f},"getPriceWithTax",{writable: true, enumerable: false, configurable: true, value:f}
}
function Log4(target:any, name:string|Symbol, position:number){// parameter decorator
comnsole.log(`Parameter decorator!`)
console.log(target, name, position); //prototype, nameOfFunction, position: {constructor:f, getPriceWithTax:f}, getPriceWithTax,0
}
Product {
@Log
title: string
_price:number
@Log2
set _price(val:number){
if(val>0){
this._price = val;
}else{
throw new Error('invalid price- should be positive');
}
}
constructor(t:string, p:number){
this.title = t;
this._price = p;
}
@Log3
getPriceWithTax(@Log4 tax:number){// parameter decorator
return this._price*(1+tax);
}
}What those parameters mean in practice:
target: prototype (instance) or constructor (static)name: member name ("getPriceWithTax","_price", etc.)descriptor: thePropertyDescriptoryou can modify (value,enumerable,configurable,get,set)position: parameter index
Also worth knowing: TypeScript’s handbook notes that decorators apply to class declarations and members (method/accessor/property/parameter). (typescriptlang.org )
Decorator execution order (two different “orders” matter)
There are two ordering rules that people mix up:
1) Multiple decorators on the same declaration (composition)
- Decorator expressions are evaluated top-to-bottom
- The resulting functions are invoked bottom-to-top (typescriptlang.org )
2) The order decorators run across a class
TypeScript defines a specific order inside a class:
- Parameter decorators, then method/accessor/property decorators for instance members
- Parameter decorators, then method/accessor/property decorators for static members
- Parameter decorators for the constructor
- Class decorators (typescriptlang.org )
This explains your observation:
“All running without instantiation. Multiple instantiation does not matter.”
Yes—most decorator code runs as the class is defined. Instantiation only matters if your class decorator returns a new constructor that adds runtime behavior.
Returning values from decorators (what is respected vs ignored)
Class decorators can replace the constructor
If a class decorator returns a value, it replaces the class constructor (so you can wrap/extend the original class). (typescriptlang.org )
Your pattern is the classic “return a subclass of the original constructor” approach:
function Decorator(){
return function<T extends {new(..._:any[]): {name:string}}>(originalConstructor:T){// sets type that extends a constructor that takes any number of arguments, and returns an object that has a name property. _ is args, ts understands that this means it won't be used.
return class extends originalConstructor {// extend so we don't lose properties already defined.
constructor(){
super(); //needed when extending
// commands here execute on instantiation
}
}
}
}This is how you:
- keep existing behavior (
extends originalConstructor) - inject extra behavior at instantiation time (inside the new
constructor())
Method/accessor decorators can return a new descriptor
Method and accessor decorators are applied to property descriptors, so you can modify/replace the method/getter/setter. (typescriptlang.org )
Property decorators cannot (in legacy TS) modify the descriptor
TypeScript’s docs explicitly call out that property decorators don’t receive a descriptor, and their return value is ignored. (typescriptlang.org ) So they’re mainly for “observe + register metadata”.
Your note matches this distinction:
function Log2(target:any, name: string, descriptor: PropertyDescriptor){
console.log(`Accessor decorator`)
console.log(`target, name, descriptor`)
return {} // can be set,get, enumerable, configurable
}One more nuance: TS disallows decorating both get and set for the same member; decorators apply to the combined descriptor, so they must go on the first accessor in source order. (typescriptlang.org )
“This is overwritten by addEventListener …”
This is a common pain point when passing methods as callbacks:
button.addEventListener('click', obj.method)losesthis(or changes it), becausemethodis called by the event system.- A method decorator is often used to auto-bind the method to the instance (by changing the descriptor’s
valueto a bound function, or returning a getter that binds on first access).
So the intent behind your note is: “watch out for lost this in callbacks; decorators can help, but event systems influence call context.”
Decorators for validation (metadata + runtime validator)
A clean “validation decorator” pattern is:
- Decorators run at class definition time and register rules
validate(obj)reads those rules and checks runtime values
High-level sketch:
function Required(){}
function PositiveNumber(){}
function validate(obj: object){
}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t:stinr,p:number){
this.title = t;
this.price = p;
}
}
// Logic for getting title and price values from form.
const createdCourse = new Course(title, price);
if(!validate(createdCourse)){// returns true if valid.
alert('Invalid input')
return;
};Then the full registry-based implementation:
interface ValidatorConfig {
[property:string]: {
[validatableProp: string]:string[] // ['required', 'positive']
}
}
const registeredValidators: ValidatorConfig = {};
function Required(target:any, propName:string){
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [...registeredValidators[target.constructor.name]?.[propName]?? []),'required'] // adds all validators that don't already exist in validator list.
}
}
function PositiveNumber(target:any, propName:string){
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [...registeredValidators[target.constructor.name]?.[propName]?? []),'positive']
}
}
function validate(obj: any){
const objValidatorConfig = registeredValidators[obj.constructor.name];
if(!objValidatorConfig){
return true;
}
let isValid = true;
for (const prop in objValidatorConfig){
for (const validator of objValidatorConfig[prop]){
switch(validator){
case 'required': isValid && !!obj[prop];break; // boolean conversion
case 'positive': isValid && obj[prop]>0;break;
}}
}
return isValid;
}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t:stinr,p:number){
this.title = t;
this.price = p;
}
}
// Logic for getting title and price values from form.
const createdCourse = new Course(title, price);
if(!validate(createdCourse)){
alert('Invalid input')
return;
};What’s happening conceptually:
-
registeredValidatorsis a global “metadata store” -
Each decorator writes into it using:
- class name → property name → list of validator tags
-
validatelooks up the class name, then checks each property against each tag
This is the essence of how many real decorator-based validation systems work (including popular libraries), except they often store richer metadata and support async rules.
References (worth bookmarking)
- TypeScript Handbook: Decorators (typescriptlang.org )
- TypeScript 5.0 release notes: new decorators vs
--experimentalDecorators(typescriptlang.org )