Skip to Content
BlogCodeUnderstanding TypeScriptUnderstanding TypeScript 4

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: the PropertyDescriptor you 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:

  1. Parameter decorators, then method/accessor/property decorators for instance members
  2. Parameter decorators, then method/accessor/property decorators for static members
  3. Parameter decorators for the constructor
  4. 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) loses this (or changes it), because method is called by the event system.
  • A method decorator is often used to auto-bind the method to the instance (by changing the descriptor’s value to 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:

  1. Decorators run at class definition time and register rules
  2. 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:

  • registeredValidators is a global “metadata store”

  • Each decorator writes into it using:

    • class name → property name → list of validator tags
  • validate looks 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)