completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Decorators

Wrap classes, methods, fields, and accessor properties with reusable behavior while staying honest about the proposal, metadata, and today's tooling.

Three things to take away
  • 01
    Wrap a methodWrite a method decorator that prints each call, or keeps this tied to its object, without changing the method's own code.
  • 02
    Change starting valuesUse a field decorator to change a starting value, and an auto-accessor decorator to also check later writes.
  • 03
    Pick 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):

  • value is 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, like count = 0. You'll meet auto-accessors later in this lesson.
  • context is an object that describes that part. It says the kind (such as "method"), the name, and whether the part is static or private. It also holds tools, such as addInitializer and metadata.
The short version

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.

Real-life analogyGift wrapping around a present

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

INTERACTIVE

A 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.
The logged decorator takes the old increment method and hands back a new method. The new method wraps the old one: it prints call increment, runs increment(2), then prints return 2.incrementold methodloggedcall incrementincrement(2)return 2new method
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:

  • logged wraps the method. The wrapper prints a line, runs the real method, then prints the result.
  • bound fixes a common problem. Say you save a method in a variable, like const click = counter.increment, and then call click(2). That call has no object in front of it, so this is lost. bound uses addInitializer to ask for setup work for each new object. The setup work calls .bind(), which makes a copy of the method whose this is always that object.
Real-life analogyThe label on a lunch box

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.kind and context.name say what is being decorated
In real life: The label has useful notes
In JavaScript: addInitializer, access, and metadata are tools the decorator can use
In real life: You read the label before you wrap
In JavaScript: The decorator reads context before it returns a wrapper
Method decorator lab: print each call and keep this
Decorator syntax (compiled in the tests)JavaScript
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));
The same steps as plain function callsPop out in the code editor (opens in a new tab)JavaScript
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));
What the model printsmethod:increment
call increment(2)return 2console 2

Final count: 2

Try it yourself

The wrapper printed a line, ran the real method, then printed the result.

The first code block shows the decorator syntax. The output comes from the second block, which does the same steps with plain function calls.

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 THROUGH

A 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 new makes an object.
Step through: building a class with decorators
Step 0 of 8Ready
Your turn: follow the blue line

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.

Running in
  1. script
Next: line 19
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));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose what the decorator sits on

Changing this starts a new replay with the matching code.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Three code lines stacked: @logged, @bound, increment(). An arrow down the left says the names are read top to bottom. An arrow up the right says the decorators are called bottom to top, so bound runs before logged.@logged@boundincrement()1. readtop to bottom2. calledbottom to top
JavaScript reads the names from the top down. Then it calls them from the bottom up, so bound gets the method first and logged wraps what bound hands back.
Two decorators on one method

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

INTERACTIVE

A 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:

  • init changes the starting value.
  • get runs on every read.
  • set runs 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.

Left, a field: percent = 135 goes through clamp, so the percent box holds 100. This happens once, at the start. Right, an auto-accessor: nickname is set to Grace with spaces around it. It goes through trimBox's set, so the nickname box holds Grace with no spaces. This happens on every write.fieldpercent = 135clamp100percentset once, at the startauto-accessornickname = "  Grace  "trimBox"Grace"nicknametrimmed on every write
The field decorator 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.
Real-life analogyA jar with a lid

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: get and set run on every read and write
In real life: You check the item before you close the lid
In JavaScript: init and set can trim, limit, or check the value
Lab: a field decorator and an auto-accessor decorator
Field and auto-accessor syntax (compiled in the tests)JavaScript
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);
The same steps as plain function callsPop out in the code editor (opens in a new tab)JavaScript
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);
Values the model storesmodel run
percent100
starting nickname"Ada"
after the new write"Grace"
Try it yourself

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.

The lab runs the same steps with a plain object. Object.defineProperty adds the getter and setter, so it works in every browser.
A field decorator can check the starting valuePop out in the code editor (opens in a new tab)JavaScript
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

INTERACTIVE

Sometimes 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.

The decorator @label("Saves draft data") writes a save note into the metadata cart. Later, the console reads Task[Symbol.metadata].save and prints Saves draft data.@label("Saves draft data")writessavemetadatasave: "Saves draft data"read laterconsoleTask[Symbol.metadata].saveSaves draft data
@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.
Metadata lab: write a note, then read it from the class
Metadata syntax (compiled in the tests)JavaScript
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);
The same steps as plain function callsPop out in the code editor (opens in a new tab)JavaScript
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);
What the class saysTask.save
Saves draft data
Try it yourself

The decorator wrote your text into the shared notes object. Later code read it back from the class with Symbol.metadata.

Metadata comes from a separate proposal, so compiled code may need to create Symbol.metadata first.

Can you use decorators today?

DETECT

Decorators 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.

Support check: can this browser read decorator syntax?
The check: try to read decorator syntaxPop out in the code editor (opens in a new tab)JavaScript
const code = "function dec(value, context) {}\n@dec class C {}";new Function(code); // parse test only
Browser resultchecking
checking...
Try it yourself
Runs once the page loads, inside a hidden, locked-down frame.

Waiting for the hidden frame to report back.

The test code is tiny and fixed by the lesson. It never runs text you type.

Each row of the next table is one place your code might run. Read across to see what to expect there.

Where decorators work today
Where your code runsWhat to expect todayWhat to do
Browsers and Node.js on their ownDon't count on them reading @ syntax.Compile your code, or test for support first. Node 22 rejects the syntax in a normal script.
TypeScriptIt compiles the decorator syntax used in this lesson.This is not the older experimentalDecorators mode that many libraries use.
BabelIt compiles decorators through a plugin.Pick the plugin version that matches your project and its libraries.
MetadataIt 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

SORTER

Ask 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.
Which decorator kind fits?
  • 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
Try it yourself
0 of 8 correct

Sort each job into the part of the class the decorator should go on.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.

A decorator changes one part, nothing elsePop out in the code editor (opens in a new tab)JavaScript
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.

Keep security checks easy to see

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.

What each kind of decorator gets and gives back
Which kindWhat the decorator getsWhat it may returnWhen its work takes effect
MethodThe method itself, plus context.A new function to use instead.Once, while the class is built. The new function then runs on every call.
Fieldundefined (a field has no value yet), plus context.A function that changes the starting value.Each time new makes an object.
Auto-accessorAn 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 experimentalDecorators mode 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 uses addInitializer, 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.metadata created first.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict what the wrapper prints

This code calls logged by hand, the way JavaScript would. What are the two lines it prints?

Starter codePop out in the code editor (opens in a new tab)JavaScript
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));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeKeep a number between 0 and 10

    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?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function clamp(initialValue) {
      return Math.min(10, Math.max(0, initialValue));
    }
    
    console.log(clamp(18));
    console.log(clamp(-3));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the missing setup step

      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.

        Exercise 4 · PracticeRead a note back

        What text does this code print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const metadata = {};
        metadata.save = "writes data";
        console.log(metadata.save);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePick the kind that sees every write

          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?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Decorators quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which two inputs does a decorator get in today's proposal?

              Choose an answer to see the explanation.

            2. Question 2 of 8This code calls logged by hand. What does it print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              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));

              Choose an answer to see the explanation.

            3. Question 3 of 8What can a field decorator return?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does the clamp code print for a high value and then a low value?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            5. Question 5 of 8Why does a binding decorator use addInitializer?

              Choose an answer to see the explanation.

            6. Question 6 of 8What is special about decorator metadata?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which advice about browser and Node.js support is safest?

              Choose an answer to see the explanation.

            8. 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.metadata to 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.

            CompleteFrontend Clear concepts. Working examples.