Decorators
Wrap classes, methods, fields, and accessor properties with reusable behavior while staying honest about the proposal, metadata, and today's tooling.
- 01Wrap a methodWrite a method decorator that prints each call, or keeps
thistied to its object, without changing the method's own code. - 02Change starting valuesUse a field decorator to change a starting value, and an auto-accessor decorator to also check later writes.
- 03Pick the right toolsTell today's decorator proposal apart from TypeScript's older decorators, and know when you must compile your code.
A decorator changes part of a class
A decorator is a function that you put on part of a class with an @ sign, like @logged. JavaScript calls it once, while it builds the class. It does not wait until you use the class.
A decorator can do these jobs:
- Look at the part it sits on.
- Swap that part for a new version, such as a method that also prints a message.
- Ask for setup work to run each time a new object is made.
- Write notes, called metadata, that other code reads later.
Every decorator gets two inputs, (value, context):
valueis the part being decorated. It can be a method, a field, an auto-accessor, or the whole class. A field is a property written in the class body, likecount = 0. You'll meet auto-accessors later in this lesson.contextis an object that describes that part. It says thekind(such as"method"), thename, and whether the part isstaticorprivate. It also holds tools, such asaddInitializerandmetadata.
Write a class add-on once, as a decorator. Then put it on any method, field, auto-accessor, or class. You no longer copy the same logging, binding, checking, or note-writing code into many places.
You wrap a present before you give it away. You can add paper, a ribbon, or a label. The present inside stays the same. A decorator works like that. It adds something to part of a class before anyone uses the class.
- In real life: A present
- In JavaScript: One part of the class, such as a method
- In real life: Wrapping paper and a ribbon
- In JavaScript: The decorator wraps the part, swaps it, or adds a note
- In real life: The gift is wrapped before the party
- In JavaScript: The class is ready before you make any objects with
new
Many browsers can't read the @ syntax yet. So this lesson does two things:
- The code blocks show real decorator syntax. The lesson's tests compile that code with TypeScript and check what it prints.
- The live labs run a model: the same steps written as ordinary function calls. The model works in every browser.
A method decorator wraps a method
INTERACTIVEA method decorator gets the original method as value. If it returns a new function, that new function takes the method's place. This helps with jobs that many methods share:
- printing a line on every call (this is called logging),
- measuring how long a call takes,
- remembering earlier results,
- trying again after a failure,
- checking that the user is allowed to do something.
logged takes the old method and hands back a new one that wraps it. For click(2), the new method prints call increment, runs the old method, then prints return 2.The lab below uses two decorators:
loggedwraps the method. The wrapper prints a line, runs the real method, then prints the result.boundfixes a common problem. Say you save a method in a variable, likeconst click = counter.increment, and then callclick(2). That call has no object in front of it, sothisis lost.boundusesaddInitializerto ask for setup work for each new object. The setup work calls.bind(), which makes a copy of the method whosethisis always that object.
Before you wrap a lunch box, you read its label. Whose lunch is it? What is inside? The context object is that label. It tells the decorator what it is working on. It also hands over a few tools.
- In real life: The label says whose lunch it is
- In JavaScript:
context.kindandcontext.namesay what is being decorated - In real life: The label has useful notes
- In JavaScript:
addInitializer,access, andmetadataare tools the decorator can use - In real life: You read the label before you wrap
- In JavaScript: The decorator reads
contextbefore it returns a wrapper
function logged(value, context) { return function (...args) { console.log("call " + String(context.name)); const result = value.apply(this, args); console.log("return " + result); return result; };} function bound(value, context) { context.addInitializer(function () { this[context.name] = this[context.name].bind(this); });} class Counter { count = 0; @logged @bound increment(step = 1) { this.count += step; return this.count; }} const counter = new Counter();const click = counter.increment;console.log(click(2));function logged(value, context) { return function (...args) { console.log("call " + String(context.name)); const result = value.apply(this, args); console.log("return " + result); return result; };} function bound(value, context) { context.addInitializer(function () { this[context.name] = this[context.name].bind(this); });} const context = { kind: "method", name: "increment", static: false, private: false, access: {}, metadata: {}, addInitializer(initializer) { initializers.push(initializer); }};const initializers = []; let increment = function (step = 1) { this.count += step; return this.count;};increment = bound(increment, context) ?? increment;increment = logged(increment, context) ?? increment; const counter = { count: 0, increment };for (const initializer of initializers) initializer.call(counter);const click = counter.increment;console.log(click(2));call increment(2)return 2console 2Final count: 2
The wrapper printed a line, ran the real method, then printed the result.
Try this. Leave saved copy on and turn bound off. The method runs without its object, so this is lost and you see an error. Turn bound back on. The setup work ties the method to the object before the saved copy is called, so it works again.
Decorators run while the class is built
STEP THROUGHA decorator does not run each time you call the method. It runs once, while JavaScript builds the class. What it leaves behind is used later:
- A new method it returned runs on every call.
- Setup work it asked for runs each time
newmakes an object.
This replay follows a hand-written model of the steps. JavaScript reads the decorator names, calls the decorators, puts in what they return, and runs setup work later, when each object is made.
script
function logged(value, context) { return function (...args) { console.log("call " + String(context.name)); const result = value.apply(this, args); console.log("return " + result); return result; };} function bound(value, context) { context.addInitializer(function () { this[context.name] = this[context.name].bind(this); });} class Counter { count = 0; @bound increment(step = 1) { this.count += step; return this.count; }} const counter = new Counter();const click = counter.increment;console.log(click(2));Pick swap a method to see logged return a wrapper. Pick change a field's start value to see a field decorator. It returns a small function. That function gets the field's starting value each time a new object is made, and returns the value to store.
bound gets the method first and logged wraps what bound hands back.JavaScript reads the decorator names from top to bottom. Then it calls the decorators from the bottom up: the one closest to the method runs first. So with @logged above @bound, bound gets the method first. Then logged wraps whatever bound handed back.
A field decorator runs once; an auto-accessor checks every write
INTERACTIVEA field decorator can change a field's starting value. For example, it can keep a number inside a range, or fill in a missing name. It runs once for each new object, when the object is made.
An auto-accessor is a field with the word accessor in front, like accessor nickname = "Ada". JavaScript keeps its value in a hidden place. It also adds a getter and a setter: small functions that run when you read or write the property. A decorator on an auto-accessor can return three functions:
initchanges the starting value.getruns on every read.setruns on every write, so it can check or clean each new value.
The lab below uses two helpers. .trim() removes spaces from the start and end of a string. Math.min and Math.max keep a number between 0 and 100.
clamp changes the starting value once: 135 becomes 100. The auto-accessor decorator trimBox trims the start value and every later write, so "Grace" is stored with no spaces.A plain field is like an item left on a desk. Anyone can swap it, and nobody checks. An auto-accessor is like a jar with a lid. Every value goes in and out through the lid. A decorator can clean or refuse a value at the lid.
- In real life: The jar holds the item
- In JavaScript: The auto-accessor keeps the value in a hidden place
- In real life: The lid controls what goes in and out
- In JavaScript:
getandsetrun on every read and write - In real life: You check the item before you close the lid
- In JavaScript:
initandsetcan trim, limit, or check the value
function clamp(value, context) { return function (initialValue) { return Math.min(100, Math.max(0, initialValue)); };} function trimBox(value, context) { return { init(initialValue) { return initialValue.trim(); }, get() { return value.get.call(this); }, set(nextValue) { value.set.call(this, nextValue.trim()); } };} class Profile { @clamp percent = 135; @trimBox accessor nickname = " Ada ";} const profile = new Profile();console.log(profile.percent);console.log(profile.nickname);profile.nickname = " Grace ";console.log(profile.nickname);const metadata = {};const percentContext = { kind: "field", name: "percent", static: false, private: false, access: {}, metadata, addInitializer() {}};const nicknameContext = { kind: "accessor", name: "nickname", static: false, private: false, access: {}, metadata, addInitializer() {}}; const clampInit = clamp(undefined, percentContext);const wrapped = trimBox({ get() { return this._nickname; }, set(value) { this._nickname = value; }}, nicknameContext); const profile = {};profile.percent = clampInit(135);profile._nickname = wrapped.init(" Ada ");Object.defineProperty(profile, "nickname", { get() { return wrapped.get.call(profile); }, set(value) { wrapped.set.call(profile, value); }});console.log(profile.percent);console.log(profile.nickname);profile.nickname = " Grace ";console.log(profile.nickname);100"Ada""Grace"clamp kept the starting percent between 0 and 100, once. trimBox removed the spaces from the starting nickname and from the new one you wrote.
Object.defineProperty adds the getter and setter, so it works in every browser.function nonEmpty(value, context) {
return function (initialValue) {
if (initialValue.trim() === "") throw new Error("name is required");
return initialValue.trim();
};
}
const name = nonEmpty(undefined, { kind: "field", name: "name" })(" Asha ");
console.log(name);nonEmpty returns a function for the starting value. That function removes the spaces around the name, so line 9 prints Asha. If the name were empty, it would throw an error instead.
A field decorator only sees the starting value. It can't see later writes to a plain field. If later writes need checking too, use an auto-accessor.
Metadata: notes a decorator leaves for other code
INTERACTIVESometimes a decorator doesn't need to wrap anything. It only writes down a fact. For example: “this method saves data”, “this field is required”, or “this class handles the home page”. Facts like these are called metadata: data about the code.
Every decorator on the same class gets the same notes object, context.metadata. After the class is built, other code reads that object from the class with Task[Symbol.metadata]. Symbol.metadata is a symbol: a special key that can't clash with any other property name.
Metadata comes from its own, separate proposal. So Symbol.metadata may not exist yet. Line 1 of the metadata code below creates it when it is missing.
@label writes a note under the key save in the class's shared metadata object. Later code reads it back from the class with Task[Symbol.metadata].save.if (!Symbol.metadata) Symbol.metadata = Symbol("Symbol.metadata"); function label(text) { return function (value, context) { context.metadata[context.name] = text; };} class Task { @label("Saves draft data") save() {}} console.log(Task[Symbol.metadata].save);const metadataKey = Symbol.metadata ?? Symbol("Symbol.metadata");const metadata = {};const context = { kind: "method", name: "save", static: false, private: false, access: {}, metadata, addInitializer() {}}; label("Saves draft data")(function save() {}, context);class Task {}Task[metadataKey] = metadata; console.log(Task[metadataKey].save);Saves draft dataThe decorator wrote your text into the shared notes object. Later code read it back from the class with Symbol.metadata.
Symbol.metadata first.Can you use decorators today?
DETECTDecorators are a proposal: a planned feature that TC39, the group that designs JavaScript, is still working on. When this lesson was written, you couldn't count on browsers or Node.js to read the @ syntax. For example, Node 22 rejected a normal script that used it.
So most projects compile decorators. A tool rewrites your code into plain JavaScript that every engine can read. Two common tools:
- TypeScript compiles the decorator syntax used in this lesson. Be careful: TypeScript also has an older mode called
experimentalDecorators. That mode passes different inputs,(target, key, descriptor), and many older libraries still use it. - Babel compiles decorators through a plugin. The plugin version must match the one your project and its libraries expect.
The check below hands a tiny decorator script to new Function. new Function turns a string of code into a function without calling it. If the browser can't read the syntax, it throws a SyntaxError.
const code = "function dec(value, context) {}\n@dec class C {}";new Function(code); // parse test onlychecking...Waiting for the hidden frame to report back.
Each row of the next table is one place your code might run. Read across to see what to expect there.
| Where your code runs | What to expect today | What to do |
|---|---|---|
| Browsers and Node.js on their own | Don't count on them reading @ syntax. | Compile your code, or test for support first. Node 22 rejects the syntax in a normal script. |
| TypeScript | It compiles the decorator syntax used in this lesson. | This is not the older experimentalDecorators mode that many libraries use. |
| Babel | It compiles decorators through a plugin. | Pick the plugin version that matches your project and its libraries. |
| Metadata | It comes from a separate proposal. | Symbol.metadata may need to be created before compiled code can store notes. |
Pick the kind of decorator by the job
SORTERAsk what you need to change:
- The whole class? Use a class decorator. It gets the class itself as
value. - A method you call? Use a method decorator.
- A field's starting value? Use a field decorator.
- Every read and write of a property? Use an auto-accessor decorator.
- Add the whole class to a framework's list of parts it can use
- Print a line on every call to
save(), then run the real method - Lower a field's starting score from 135 to 100
- Trim spaces from a property when the object is made, and on every write
- Note which web page a whole class handles
- Remember the result of
total()so later calls reuse it - Use
"draft"when a field's starting value is empty - Refuse later writes of a blank name
Sort each job into the part of the class the decorator should go on.
Where you will use decorators
Decorators help most when the same extra job shows up in many places. You write the job once and put it on each place with @. The method itself stays short. Common uses:
- UI classes bind methods once, so callbacks don't lose
this. (A callback is a function you hand to other code to call later.) - Data classes trim or limit values before they store them.
- Frameworks collect notes about pages or checks while the class is built.
- Libraries remember the results of slow methods, without changing the method's code.
These ideas build on earlier lessons: class basics, private members, getters and setters, closures, and higher-order functions.
function describe(value, context) {
return function (...args) {
console.log(context.name + " called");
return value(...args);
};
}
const save = describe((order) => "saved " + order, {
kind: "method",
name: "save",
});
console.log(save("tea"));
console.log(typeof save.extra);The wrapper changes how save runs: it prints save called before line 12 prints saved tea. It doesn't add anything new. So on line 13, typeof save.extra is "undefined".
A decorator can only do these things to the part it sits on:
- swap it for a new one of the same kind, such as a new method for a method,
- ask for setup work,
- write metadata.
It can't add new methods to the class or change its other parts.
A decorator can hide logging, checks, or permission rules inside a wrapper. That saves copying. But someone who reads the method call can't see those rules. Keep security checks easy to find. Test the decorated method, not only the plain function.
Common misconceptions
Each row of the table is one kind of decorator, or the metadata tool. Read across to see what the decorator gets, what it may return, and when its work takes effect.
| Which kind | What the decorator gets | What it may return | When its work takes effect |
|---|---|---|---|
| Method | The method itself, plus context. | A new function to use instead. | Once, while the class is built. The new function then runs on every call. |
| Field | undefined (a field has no value yet), plus context. | A function that changes the starting value. | Each time new makes an object. |
| Auto-accessor | An object with the hidden get and set, plus context. | New get, set, and init functions, or only some of them. | On the starting value, every read, and every write. |
| Metadata (a tool, not a kind) | The same context, which holds metadata. | Usually nothing. | Written while the class is built. Read later from the class. |
- “Decorators are just comments.” They are functions, and they can change how the class works.
- “Every browser runs them today.” Compile your code first, or test whether the browser can read the syntax.
- “Old TypeScript decorators are the same thing.” The older
experimentalDecoratorsmode passes(target, key, descriptor), a different set of inputs. - “A field decorator sees the field’s value right away.” It gets
undefined. It returns a function, and that function sees each object’s starting value later. - “A method can be bound before any object exists.”
.bind()needs an object. So a binding decorator usesaddInitializer, whose work runs when each object is made. - “Metadata is filled in for you.” Each decorator decides what to write. Some engines also need
Symbol.metadatacreated first.
Practice exercises
5 EXERCISESThis code calls logged by hand, the way JavaScript would. What are the two lines it prints?
function logged(value, context) {
return function (...args) {
console.log("call " + String(context.name));
return value.apply(this, args);
};
}
const context = { kind: "method", name: "total", static: false, private: false, access: {}, metadata: {}, addInitializer() {} };
const original = function (a, b) { return a + b; };
const total = logged(original, context);
console.log(total(2, 3));The wrapper prints call total first. Then it runs the original function, which returns 5. So the last line prints 5.
clamp works like the function a field decorator returns: it gets a starting value and returns the value to store. What two numbers does this code print?
function clamp(initialValue) {
return Math.min(10, Math.max(0, initialValue));
}
console.log(clamp(18));
console.log(clamp(-3));18 is above 10, so it becomes 10. -3 is below 0, so it becomes 0.
A learner wrote a bound decorator, but calls to a saved copy of the method still fail. Which step is missing? Write the main line of the fix.
context.addInitializer(function () {
this[context.name] = this[context.name].bind(this);
});Use context.addInitializer. The function you pass runs for each new object. It replaces the method with a copy tied to that object.
What text does this code print?
const metadata = {};
metadata.save = "writes data";
console.log(metadata.save);Line 2 stores "writes data" under the key save. Line 3 reads metadata.save, so it prints writes data.
You need to trim a property when the object is made, and again every time someone assigns a new value. Which kind of decorator fits?
Use an auto-accessor decorator. Its init trims the starting value, and its set trims every later write.
Check your understanding
8 QUESTIONSQuestion 1 of 8Which two inputs does a decorator get in today's proposal?
Choose an answer to see the explanation.
Question 2 of 8This code calls
loggedby hand. What does it print first?Read the code, then predictfunction logged(value, context) { return function (...args) { console.log("call " + String(context.name)); return value.apply(this, args); }; } const context = { kind: "method", name: "total", static: false, private: false, access: {}, metadata: {}, addInitializer() {} }; const original = function (a, b) { return a + b; }; const total = logged(original, context); console.log(total(2, 3));Choose an answer to see the explanation.
Question 3 of 8What can a field decorator return?
Choose an answer to see the explanation.
Question 4 of 8What does the clamp code print for a high value and then a low value?
Read the code, then predictfunction clamp(initialValue) { return Math.min(10, Math.max(0, initialValue)); } console.log(clamp(18)); console.log(clamp(-3));Choose an answer to see the explanation.
Question 5 of 8Why does a binding decorator use
addInitializer?Choose an answer to see the explanation.
Question 6 of 8What is special about decorator metadata?
Choose an answer to see the explanation.
Question 7 of 8Which advice about browser and Node.js support is safest?
Choose an answer to see the explanation.
Question 8 of 8Which job needs an auto-accessor decorator?
Choose an answer to see the explanation.
Key takeaways
- A decorator is a function that JavaScript calls once, while it builds the class.
- Each decorator gets
(value, context). TypeScript's older mode passed(target, key, descriptor)instead. - A method decorator can return a new function to use instead of the method. A field decorator can return a function that changes the starting value.
- An auto-accessor decorator can change the starting value, every read, and every write.
- Metadata comes from a separate proposal. It may need
Symbol.metadatato be created first. - Compile decorator code, or test for support, before you ship it.
A decorator is a function that runs while a class is built. It can wrap, set up, or add notes to the class or one of its parts.
Up next: eval & new Function.