Skip to content
TypeScript ts decorators 4 min read

Property Decorators

A property (field) decorator attaches to a class field and lets you intercept how that field is initialized or accessed. In the Stage 3 (TypeScript 5.0) standard, a field decorator can return an initializer transformer — a function that receives the field’s initial value and returns the value actually stored — which is how you enforce defaults, validation, or transformation. The legacy experimentalDecorators form instead just observes the field and is typically paired with reflect-metadata to record its type. This page shows both forms and the patterns each enables.

Stage 3 field decorators

A standard field decorator is (value, context) where value is always undefined (fields have no value at decoration time) and context is a ClassFieldDecoratorContext. Returning a function turns it into an initializer transformer.

function clamp(min: number, max: number) {
  return function (
    _value: undefined,
    _context: ClassFieldDecoratorContext<unknown, number>,
  ) {
    return function (initial: number) {
      return Math.max(min, Math.min(max, initial));
    };
  };
}

class Volume {
  @clamp(0, 100) level = 150;
}

new Volume().level; // 100 — the initializer clamped it

The inner function receives the field’s initial value (150) and returns the stored value (100). This runs for every instance, so each new object gets its field transformed.

The field decorator context

ClassFieldDecoratorContext cannot wrap reads and writes directly, but it exposes access (with both get and set) and addInitializer for per-instance setup.

MemberTypePurpose
kind"field"Discriminates the target
namestring | symbolThe field’s key
staticbooleanWhether the field is static
privatebooleanWhether the field is #private
access{ get, set }Read/write the field on an instance
addInitializer(fn) => voidRun per-instance setup
function trim(_: undefined, context: ClassFieldDecoratorContext<unknown, string>) {
  return (initial: string) => initial.trim();
}

class Form {
  @trim username = "  ada  ";
}

new Form().username; // "ada"

Because the transformer only sees the initializer, use a getter/setter or an accessor decorator when you need to intercept every later assignment, not just the first.

Turning a field into an accessor

To validate on every write, combine the new accessor keyword with a decorator. An accessor field is backed by a getter/setter pair, and its decorator can return replacement get/set functions.

function positive(
  _value: { get: () => number; set: (v: number) => void },
  _context: ClassAccessorDecoratorContext<unknown, number>,
) {
  return {
    set(this: unknown, value: number) {
      if (value < 0) throw new RangeError("must be >= 0");
      _value.set.call(this, value);
    },
  };
}

class Account {
  @positive accessor balance = 0;
}

const a = new Account();
a.balance = 50;   // ✅ ok
a.balance = -1;   // ❌ throws RangeError at runtime

The accessor keyword generates a private backing field plus a getter and setter, and the decorator overrides the setter to enforce the invariant on every assignment.

Legacy property decorators

Under experimentalDecorators, a property decorator receives only (target, propertyKey) — no descriptor, no value. It cannot change the field; it is used to record metadata about it, usually via reflect-metadata.

// tsconfig: "experimentalDecorators": true, "emitDecoratorMetadata": true
import "reflect-metadata";

function Column(target: object, propertyKey: string) {
  const type = Reflect.getMetadata("design:type", target, propertyKey);
  console.log(`${propertyKey} is a ${type.name}`); // e.g. "title is a String"
}

class Post {
  @Column title!: string;
}

With emitDecoratorMetadata on, the compiler emits a design:type entry the decorator can read. This is exactly how TypeORM’s @Column and NestJS DTOs infer property types — see Metadata & Reflection.

Stage 3 vs legacy at a glance

AspectStage 3 fieldLegacy property
Arguments(value, context)(target, propertyKey)
Can transform valueyes (initializer fn)no
Needs reflect-metadatanousually yes
Used bynew code, Angular 16+TypeORM, NestJS

Best Practices

  • Return an initializer function from Stage 3 field decorators to set defaults or sanitize input.
  • Use accessor + a decorator when you must validate on every assignment, not just init.
  • Prefer field decorators for value transformation; reserve metadata decorators for the legacy world.
  • Keep transformers pure and fast — they run once per instance during construction.
  • In legacy code, enable emitDecoratorMetadata so design:type is available to read.
  • Do not assume the field value is available at decoration time; in Stage 3 it is always undefined.
  • Pick one decorator system per project to avoid incompatible signatures.
Last updated June 29, 2026
Was this helpful?