cf.completefrontendCode editorOpen lab
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 behavior safelyModel method decorators that log calls or bind this without changing every method body.
  • 02
    Initialize data deliberatelyUse field and auto-accessor decorators to transform initial values and guard later writes.
  • 03
    Choose tooling with careSeparate the current TC39 proposal from older TypeScript decorator behavior and native support.

Decorators are class finishing tools

A decorator is a function that receives part of a class while the class is being defined. It can look at the part, wrap it, replace it, register setup work, or attach metadata for other code to read later.

The current proposal-style shape is (value, context). value is the method, field, auto-accessor, or class piece being decorated. context is a label and toolbox: it tells the decorator the kind, name, whether the member is static or private, and exposes tools such as addInitializer and metadata.

The short version

Decorators let you write reusable class add-ons once, then apply them to methods, fields, auto-accessors, or classes instead of copying the same logging, binding, validation, or metadata code.

Real-life analogyGift wrapping around a present

Imagine wrapping a present before giving it away. The wrapping can add a ribbon, a label, or an instruction. A decorator adds behavior before you create instances of the class.

In real life: A present
In JavaScript: A class element while the class is defined
In real life: Wrapping and a ribbon
In JavaScript: A decorator wraps, replaces, initializes, or annotates
In real life: The finished gift
In JavaScript: The class is ready before you create instances

Where the analogy stops: Wrapping is only a picture. Decorators run during class definition, while instance initializers run later.

This lesson keeps the runnable demos in plain JavaScript. The code blocks show decorator syntax, and the tests compile that syntax with TypeScript, but the live labs run a desugared model made from ordinary function calls. That keeps the page honest in browsers that cannot parse decorator syntax yet.

Method decorators wrap behavior

INTERACTIVE

A method decorator receives the original function. If it returns a new function, that new function becomes the method. That is perfect for logging, timing, memoizing, retrying, or checking permissions.

The lab below shows two common decorators. logged wraps a method so it prints before and after the real call. bound registers an initializer so each instance binds its method once, which matters when a method is detached from its object.

Real-life analogyThe label on a lunch box

Before wrapping, you need to know what is in the lunch box. Is it a method? What is its name? Is it static or private? The context object answers those questions and supplies safe tools.

In real life: A label says whose lunch it is
In JavaScript: context.kind and context.name identify what is decorated
In real life: The label includes useful notes
In JavaScript: addInitializer, access, and metadata are decorator tools
In real life: Read the label before wrapping
In JavaScript: The decorator reads context before returning a wrapper

Where the analogy stops: A real label cannot do anything. A decorator context also has callable tools.

Method decorator lab: log calls and bind this
Displayed decorator syntaxJavaScript
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));
Runnable desugared modelPop 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));
Runtime outputmethod:increment
call increment(2)return 2console 2

Final count: 2

Try it yourself

The wrapper logged the call, ran the original method, then logged the result. Binding keeps the detached method attached to its instance.

The visible syntax is shown first. The live output comes from the proposal-shaped desugared model beside it.

Try turning bound off while the detached call is on. The method is called without its object, so this is lost. Turn bound back on and the initializer fixes the instance before the detached call happens.

What the engine does while defining the class

STEP THROUGH

Decorators are not called each time you call a method. They run while the class is being defined. The replacement method they return, or the initializer they register, affects what happens later.

Step through class definition time
Step 0 of 8Ready
Your turn: follow the blue line

This is a replay of a hand-written model of class definition time: decorator expressions are evaluated, decorators are called, replacements are installed, and instance initializers run later.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the decorated member to replay

Changing the setting starts a fresh replay with source from the selected case.

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.

In the method version, the decorator returns a wrapper function. In the field version, the decorator returns an initializer transform. The transform receives the field’s starting value every time a new instance is created.

Read stacked decorators carefully

Decorator expressions are collected from top to bottom, but calls are applied from the closest decorator outward. If two wrappers both return functions, the top wrapper usually sees the function returned by the lower wrapper.

Fields initialize once; auto-accessors can guard future writes

INTERACTIVE

A field decorator is great when you only need to transform the initial value. A number can be clamped as an instance is created, or a missing string can be replaced with a default.

An auto-accessor creates a hidden storage slot with a public getter and setter door. A decorator can wrap the door: transform the starting value with init, change reads with get, and guard writes with set.

Real-life analogyA jar with a lid

A plain field puts a value straight on a desk. An auto-accessor is more like a jar with a lid: values go through the lid, and a decorator can clean or reject them.

In real life: A jar holds the item
In JavaScript: The auto-accessor has hidden storage
In real life: A lid controls access
In JavaScript: get and set control reads and writes
In real life: A check before closing the lid
In JavaScript: init or set can trim, clamp, or validate

Where the analogy stops: The storage slot comes from the language, not a jar you can open directly. Decorators get only the allowed hooks.

Field decorator and auto-accessor lab
Displayed field and auto-accessor syntaxJavaScript
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);
Runnable desugared modelPop 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 after decoratorsmodel run
percent100
initial nickname"Ada"
after assignment"Grace"
Try it yourself

The field initializer clamps the starting percent once. The auto-accessor wrapper trims the initial nickname and the later assignment.

The lab mirrors the displayed code with plain objects and property descriptors so it can run everywhere.

Decorator metadata is information for other code

INTERACTIVE

Sometimes a decorator does not need to wrap anything. It just records facts: “this method saves data,” “this field is required,” or “this class handles this route.” That is metadata.

The metadata proposal gives every decorator on the same class a shared context.metadata object. After the class is defined, code can read that object from the class through Symbol.metadata. Because metadata is a separate proposal, compiled examples may need a small Symbol.metadata fallback.

Metadata lab: write a label, read it from the class
Displayed metadata syntaxJavaScript
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);
Runnable metadata modelPop 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);
Metadata readoutTask.save
Saves draft data
Try it yourself

The decorator writes to the shared metadata object. Later code can read that object from the class using the metadata symbol.

Metadata is a separate proposal; compiled code may need a small Symbol.metadata fallback.

Proposal status, TypeScript, Babel, and native support

DETECT

At the time of writing, treat decorators as syntax you compile, not syntax every JavaScript engine will parse directly. I checked the TC39 proposal materials, the decorator-metadata README, and Node 22. Node 22 rejected a normal script containing decorator syntax.

TypeScript can compile the proposal-style syntax used in the examples in this lesson. That is different from TypeScript’s older experimentalDecorators mode, which used the legacy (target, key, descriptor) shape. Babel can also compile decorators, but you must choose a plugin/version that matches your codebase.

Support detector: can this browser parse decorator syntax?
Detector script ideaPop 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 after mount in a sandboxed iframe.

Waiting for the iframe to report a parse result.

The detector is intentionally tiny and predefined. It never evaluates learner text.
Support and tooling
EnvironmentWhat to expect todayPractical note
Native JavaScript enginesDo not assume on-by-default supportFeature-detect or compile; Node 22 rejects the syntax in a normal script.
TypeScriptCompiles the current proposal-style syntaxThis is different from the older experimentalDecorators mode used by many libraries.
BabelCan compile proposal versions through pluginsChoose the version that matches your codebase and framework.
MetadataA separate proposalSymbol.metadata may need a polyfill before compiled code can store metadata.

Choose the decorator kind by the job

SORTER

Ask what you need to change. Do you need the whole class, a callable method, a starting field value, or a property door that sees future reads and writes?

Which decorator kind fits?
  • Register the whole class with a framework container
  • Log every call to save() and then run the original method
  • Clamp an instance field's starting score from 135 down to 100
  • Trim a public property when it is first created and whenever it is set
  • Attach route metadata to a controller class
  • Memoize total() so repeated method calls reuse a cached result
  • Replace an empty field's initial value with "draft"
  • Reject future writes to a public name when the value is blank
Try it yourself
0 of 8 correct

Sort each requirement into the class element a decorator should target.

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

Where you will use decorators

Decorators shine when a cross-cutting behavior appears in many places: logging every command method, binding event handlers, validating values, annotating routes, or describing fields for serialization. They keep the core method small while the reusable wrapper handles the repeated concern.

  • UI classes can bind methods once so callbacks do not lose this.
  • Models can clamp or normalize incoming data before storing it.
  • Frameworks can collect route or validation metadata at class definition time.
  • Libraries can memoize expensive methods without rewriting the method body.

These ideas connect to earlier lessons on class basics, private members, getters and setters, closures, and higher-order functions.

Common misconceptions

Decorator behaviors compared
FeatureWhat the decorator receivesWhat it may returnWhen the effect happens
MethodThe function plus contextA replacement functionClass definition time; the wrapper runs on each call
Fieldundefined plus contextAn initializer transformWhen each instance initializes that field
Auto-accessorAn object with get and set plus contextget, set, and/or initReads, writes, and initial value setup
MetadataThe same context object includes metadataUsually nothingWritten during decoration, read later from the class
  • “Decorators are just comments.” They are functions that can change behavior.
  • “Every engine runs them today.” Compile or feature-detect; do not assume native parsing.
  • “Old TypeScript decorators are the same thing.” Legacy decorators use a different call shape.
  • “A field decorator sees the field’s value immediately.” It returns an initializer that sees each instance’s value later.
  • “Binding can happen before instances exist.” Binding needs addInitializer because each instance has its own method value.
  • “Metadata is automatic reflection.” Decorators must choose what to write, and metadata support may need a fallback.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a logged wrapper

Read the desugared method decorator. What are the two console lines?

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 · PracticeClamp a field initializer

    Predict the two numbers printed by this field-initializer model.

    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 binding step

      A learner wrote a bound decorator but detached calls still fail. Explain the missing step and write the core line of the fix.

        Exercise 4 · PracticeRead metadata

        What string does this metadata lookup 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 · ChallengeChoose the kind for future writes

          You need to trim a property when the instance is created and again every time someone assigns to it. Which decorator kind 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 signature matches the current proposal-style decorator shape?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the desugared logged method 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 exercise print for high and low values?

              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 statement is safest about native support?

              Choose an answer to see the explanation.

            8. Question 8 of 8Which need belongs to an auto-accessor decorator?

              Choose an answer to see the explanation.

            Key takeaways

            • Decorators are functions called while a class is being defined.
            • Proposal-style decorators receive (value, context), not the older legacy TypeScript descriptor shape.
            • Methods may return replacement functions; fields may return initializer transforms.
            • Auto-accessors let decorators wrap initial value setup, reads, and writes.
            • Metadata is separate and may require Symbol.metadata support.
            • Compile or feature-detect before shipping decorator syntax.

            A decorator is a class-definition-time function that can wrap, initialize, or annotate a class or class element.

            Up next: eval & new Function.

            CompleteFrontend Clear concepts. Working examples.