Menu

TypeScript Decorators: Method, Class, Field and Accessor

Decorators are functions that wrap or replace class members with the @ syntax. Learn the standard decorators TypeScript supports without any flag (class, method, getter, field and accessor), decorator factories, addInitializer, and how they differ from the legacy experimentalDecorators used by Angular and NestJS.

This page includes runnable editors - edit, run, and see output instantly.

A decorator is a function you attach to a class or a class member with @name. It receives the original method (or class, or field) plus a context object describing it, and it can return a replacement. TypeScript supports the standard decorators with no compiler flag.

@logged runs once, when the class is defined, and replaces add with the wrapper it returns. Every call goes through the wrapper. The generic parameters keep the method's this, argument and return types intact, so add still takes two numbers and returns a number.

How Decorators Compile

Standard decorators come from a TC39 proposal for JavaScript, and TypeScript has implemented them since TypeScript 5.0. The proposal is not part of the JavaScript standard yet, and Node 24 does not parse the @ syntax, so when the target is ES2022 (as on these pages) the compiler rewrites each decorated class into plain JavaScript that calls helper functions (__esDecorate and __runInitializers, emitted at the top of the file). The output runs anywhere ES2022 runs.

One consequence: a file with decorators cannot run under Node's built-in type stripping (node file.ts), which only removes types and leaves the @ in place. Node stops with SyntaxError: Invalid or unexpected token. Compile it with tsc or a bundler first.

Decorator Kinds and Their Signatures

Every standard decorator has the shape (value, context) => replacement | void. What value is, and what you may return, depends on what is decorated:

DecoratesvalueContext typeReturn
classthe classClassDecoratorContexta replacement class, or nothing
methodthe methodClassMethodDecoratorContexta replacement method
getter / setterthe getter or setterClassGetterDecoratorContext / ClassSetterDecoratorContexta replacement getter or setter
fieldundefinedClassFieldDecoratorContexta function that maps the initial value
accessor field{ get, set }ClassAccessorDecoratorContext{ get?, set?, init? }

Every context object has kind, name and addInitializer. A class member's context also has static, private and an access object for reading the member from an instance. Decorators work on static and #private members too.

Decorator Factories

To pass options, write a function that returns a decorator and call it at the @ site. This is a decorator factory:

@retry(3) calls retry first, and the function it returns is the actual decorator. Multiple decorators stack: in @a @b method(), b is applied first and a wraps the result.

Class Decorators and addInitializer

A class decorator receives the class itself. It can return a subclass to replace it, or return nothing and just record it somewhere. context.addInitializer registers code to run at a specific moment: for a class decorator, right after the class is fully defined; for a method decorator, when each instance is constructed.

The class decorator goes above the class (or after export). Without @bound, calling loose() would throw, because this would be undefined.

Field and Accessor Decorators

A field decorator cannot see or intercept later assignments: its value is undefined, and all it can return is a function that transforms the field's initial value. To intercept reads and writes, declare the field with the accessor keyword, which turns it into a getter and setter pair backed by private storage, and decorate that:

accessor is part of the same proposal. It emits a real getter and setter over a #private field, which is why p.price = -5 goes through the decorator's set.

Standard vs Legacy experimentalDecorators

Before TypeScript 5.0, the only decorators TypeScript had were an early version of the proposal, enabled with experimentalDecorators. That flag still exists, and it switches the compiler to the legacy model, with different signatures and semantics:

Standard (no flag)Legacy (experimentalDecorators)
Signature(value, context)(target, propertyKey, descriptor)
How it changes a methodreturns a new functionmutates descriptor.value
Parameter decoratorsnot supported (TS1206)supported
emitDecoratorMetadatanot supportedsupported (runtime type info via reflect-metadata)
accessor decorators ({ get, set, init }), addInitializeryesno
Based onthe TC39 proposalan older draft of it

A decorator written for one model does not type-check in the other. Here is a legacy-style decorator in a project without the flag:

index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.

The fix is either to rewrite it in the standard (value, context) form, as in the first example on this page, or to turn on the legacy model for the whole project:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Angular, NestJS and TypeORM still set experimentalDecorators in the configs their tools generate and their docs ask for. NestJS and TypeORM also need emitDecoratorMetadata, because they read types at runtime: NestJS to inject constructor parameters, TypeORM to map properties to columns:

// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
    constructor(@Inject(DB) private db: Database) {}
}

If you use one of those frameworks, write decorators the legacy way and follow its docs. For new code with no such framework, use the standard decorators.

When to Use Decorators

Decorators fit cross-cutting behavior that would otherwise be repeated in many methods: logging, timing, caching, retries, access checks, validation, and registering classes with a container or router. They also hide control flow, since a reader has to look up what @retry does before knowing what the method does. For one or two uses, a plain higher-order function (const fetchData = retry(3, rawFetch)) is simpler and works outside classes too.

Frequently Asked Questions

What are decorators in TypeScript?

A decorator is a function applied to a class or class member with @name syntax. It receives the thing being decorated and a context object, and can return a replacement: a wrapped method, a new class, or a function that transforms a field's initial value. Common uses are logging, validation, caching and registering classes.

Do I need experimentalDecorators to use decorators in TypeScript?

No. Since TypeScript 5.0, the standard (TC39) decorators work with no flag. experimentalDecorators switches the compiler to the older, legacy decorator model, which frameworks such as Angular and NestJS are built on. The two use different function signatures and are not interchangeable.

What is the difference between standard decorators and experimentalDecorators?

Standard decorators receive (value, context) and return a replacement. Legacy decorators receive (target, propertyKey, descriptor) and mutate the property descriptor. Only the legacy model supports parameter decorators and emitDecoratorMetadata; only the standard model has accessor decorators that return { get, set, init } and context.addInitializer.

Does TypeScript support parameter decorators?

Only with experimentalDecorators enabled. In standard mode a decorator on a parameter is error TS1206: Decorators are not valid here, because the TC39 proposal does not include parameter decorators. Dependency injection frameworks that decorate constructor parameters therefore need the legacy flag.

In what order are multiple decorators applied?

Decorator expressions are evaluated top to bottom, but applied bottom to top: in @a @b method(), b wraps the method first and a wraps the result. So a runs outermost when the method is called.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED