Understanding the New Decorator Syntax in TypeScript 5.0: History, Differences, and Practical Examples
This article explores the evolution of JavaScript decorators, the major changes introduced in TypeScript 5.0 beta—including the shift from stage 1 to stage 3 syntax—provides detailed comparisons of class, method, and property decorators, and offers practical code examples for migrating existing code.
Recently TypeScript released the 5.0 beta version, bringing many new features, the most important of which is a complete rewrite of the decorator syntax. The article examines the differences between the old and new decorator usage and briefly reviews the history of the decorator proposal.
The decorator pattern is a classic design pattern that allows adding or removing functionality from a class, method, or function without modifying its source code. Modern languages such as JavaScript/TypeScript provide built‑in syntax support, and many frameworks rely heavily on decorators.
Decorator history
2014‑04‑10: Yehuda Katz and Ron Buckton submit the decorator proposal to TC39 (stage 0).
2014‑10‑22: Angular 2.0 announces AtScript, which compiles to JavaScript and Dart and supports three kinds of decorators.
2015‑01‑28: Yehuda Katz discusses decorators with the TypeScript team.
2015‑03‑05: TC39 moves the decorator proposal to stage 1.
2015‑07‑20: TypeScript 1.5 ships with --experimentalDecorators, supporting stage 1 decorators.
2016‑07‑28: Proposal reaches stage 2 (limited adoption).
2018‑08‑27: Babel 7.0.0 adds support for stage 2 decorators via @babel/plugin-proposal-decorators.
2022‑03‑28: Chris Garrett helps push the proposal to stage 3 and separates metadata into a stage 2 proposal.
2023‑01‑26: TypeScript 5.0 beta releases, supporting stage 3 decorators.
What changed in TypeScript 5.0?
Supported entities : stage 1 only decorates classes, class properties, and class methods; stage 3 additionally supports getters, setters, and accessors.
Parameters : stage 1 decorators receive the target (or descriptor) and can use Object.defineProperty. Stage 3 decorators receive a context object containing kind, name, static, private, and an addInitializer function.
Return values : stage 1 returns a descriptor; stage 3 returns the decorated entity itself (or void).
Class decorator examples
Stage 1 simple class decorator that adds a static field and a prototype method:
const addField = target => {
target.age = 17;
target.prototype.speak = function() {
console.log('xxx');
};
};
@addField
class People {}
console.log(People.age);
const a = new People();
a.speak();Stage 3 class decorator signature:
type ClassDecorator = (
value: Function,
context: {
kind: 'class';
name: string | undefined;
addInitializer(initializer: () => void): void;
}
) => Function | void;The context provides kind (always "class" here) and an addInitializer that runs after the class is fully defined.
Method decorator examples
Stage 1 method decorator receives target, name, and descriptor. A typical logging decorator modifies descriptor.value:
function trace(_target, _name, descriptor) {
const value = descriptor.value;
descriptor.value = async function() {
console.log('Hi,ConardLi!');
console.log('start');
value.call(this);
console.log('end');
};
return descriptor;
}
class People {
@trace
test() { console.log(this); }
}
const p = new People();
p.test();Stage 3 method decorator receives value (the original function) and a richer context:
type ClassMethodDecorator = (
value: Function,
context: {
kind: 'method';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;Example using the new signature:
function trace(value, { kind, name }) {
if (kind === 'method') {
return function(...args) {
console.log('Hi,ConardLi!');
console.log(`CALL ${name}: ${JSON.stringify(args)}`);
const result = value.apply(this, args);
console.log('=> ' + JSON.stringify(result));
return result;
};
}
}
class People {
@trace
test() { console.log(this); }
}
const p = new People();
p.test();The addInitializer can be used to bind this for methods, solving the common "lost this" problem:
function bind(value, { kind, name, addInitializer }) {
if (kind === 'method') {
addInitializer(function() {
this[name] = value.bind(this);
});
}
}
class People {
@bind
toString() { return `My name is (${this.name})`; }
}Property decorator examples
Stage 1 property decorator works with the descriptor to make a field read‑only:
function readOnly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
@readOnly name = 'ConardLi';
}Stage 3 property decorator receives value (always undefined) and a context that includes access (getter/setter) and addInitializer. It can return a function that transforms the initial value:
function addPrefix() {
return initialValue => `Hi,I am ${initialValue}`;
}
class People {
@addPrefix
name = 'ConardLi';
}
const p = new People();
console.log(p.name); // Hi,I am ConardLiA more complex read‑only field implementation collects field names during class‑field decoration and applies Object.defineProperty in the class decorator:
const readOnlyFieldKeys = Symbol('readOnlyFieldKeys');
function readOnly(value, { kind, name }) {
if (kind === 'field') {
return function() {
if (!this[readOnlyFieldKeys]) this[readOnlyFieldKeys] = [];
this[readOnlyFieldKeys].push(name);
};
}
if (kind === 'class') {
return function(...args) {
const inst = new value(...args);
for (const key of inst[readOnlyFieldKeys]) {
Object.defineProperty(inst, key, { writable: false });
}
return inst;
};
}
}
@readOnly
class People {
@readOnly name;
constructor(name) { this.name = name; }
}Auto accessor (automatic getter/setter)
The new accessor keyword creates an automatic accessor that can be decorated. Example of a read‑only accessor that can be set only once:
const UNINITIALIZED = Symbol('UNINITIALIZED');
function readOnly({ get, set }, { name, kind }) {
if (kind === 'accessor') {
return {
init() { return UNINITIALIZED; },
get() {
const value = get.call(this);
if (value === UNINITIALIZED) {
throw new TypeError(`Accessor ${name} hasn't been initialized yet`);
}
return value;
},
set(newValue) {
const oldValue = get.call(this);
if (oldValue !== UNINITIALIZED) {
throw new TypeError(`Accessor ${name} can only be set once`);
}
set.call(this, newValue);
}
};
}
}
class People {
@readOnly
accessor name;
constructor(name) { this.name = name; }
}
const p = new People('ConardLi');
p.name = 'Bob'; // throws TypeErrorGetter/Setter decorators and lazy evaluation
A getter decorator can implement lazy evaluation by computing the value once and then redefining the property:
function lazy(value, { kind, name }) {
if (kind === 'getter') {
return function() {
const result = value.call(this);
Object.defineProperty(this, name, { value: result, writable: false });
return result;
};
}
}
class People {
@lazy
get value() {
console.log('computing...');
return 'computed result';
}
}
const inst = new People();
console.log(inst.value); // computes and logs
console.log(inst.value); // returns cached result without recomputingThese examples demonstrate how the stage 3 decorator API provides richer metadata, better type safety, and more flexible initialization patterns compared with the older stage 1 approach.
Do you find the new decorators useful? How difficult is the migration from the old syntax? Feel free to discuss in the comments.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
