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.
| Member | Type | Purpose |
|---|---|---|
kind | "field" | Discriminates the target |
name | string | symbol | The field’s key |
static | boolean | Whether the field is static |
private | boolean | Whether the field is #private |
access | { get, set } | Read/write the field on an instance |
addInitializer | (fn) => void | Run 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
| Aspect | Stage 3 field | Legacy property |
|---|---|---|
| Arguments | (value, context) | (target, propertyKey) |
| Can transform value | yes (initializer fn) | no |
| Needs reflect-metadata | no | usually yes |
| Used by | new 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
emitDecoratorMetadatasodesign:typeis 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.