TypeScript Property Decorators

Property decorators in TypeScript offer a powerful way to add metadata or change the behavior of class properties declaratively. Instead of writing boilerplate code inside your constructor or methods, you can "annotate" properties to handle tasks like validation, logging, or data transformation. This approach leads to cleaner, more maintainable code by separating your business logic from cross-cutting concerns.

Developer Tip: Think of decorators as "wrappers." They allow you to intercept how a property is defined or accessed without cluttering the main logic of your class.

 

What is a Property Decorator?

A property decorator is a specialized function that sits right above a class property. Unlike method decorators, property decorators do not have access to the property descriptor (like value or writable) as an argument. Instead, they are called when the class is defined, not when an instance is created.

The decorator function receives exactly two arguments:

  1. target: This is the prototype of the class (for instance properties) or the constructor function (for static properties).
  2. propertyKey: The actual name of the property as a string or symbol.
Watch Out: Property decorators execute when the script is first loaded (at class definition time), not when you create a new instance of the class using new.

 

Enabling Property Decorators in TypeScript

By default, TypeScript considers decorators an experimental feature. To use them, you must explicitly enable them in your project configuration.

Example: Enabling Property Decorators

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
  • The experimentalDecorators flag is required to prevent the TypeScript compiler from throwing errors when it encounters the @ symbol.

 

Syntax of Property Decorators

A property decorator is simply a function. Because it doesn't return a property descriptor, it's often used to record metadata about the property or to redefine the property on the prototype using Object.defineProperty.

function PropertyDecorator(target: any, propertyKey: string) {
  // 'target' is the prototype
  // 'propertyKey' is the name of the variable
  console.log("Decorating:", propertyKey);
}
Common Mistake: Beginners often try to access this inside the decorator function. Because the decorator runs at class definition time, this does not refer to an instance of the class.

 

Applying Property Decorators

Applying a decorator is as simple as prefixing the property name with @ followed by the function name.

Example: Applying a Simple Property Decorator

function LogProperty(target: any, propertyKey: string) {
  console.log(`Property "${propertyKey}" has been registered.`);
}

class Example {
  @LogProperty
  name: string;
}

// Console Output: Property "name" has been registered.
const example = new Example();
  • The @LogProperty decorator runs as soon as the Example class is parsed by the JavaScript engine.

 

Modifying Property Behavior with Decorators

Since property decorators don't give you direct access to the property value, you must use Object.defineProperty to intercept the get and set actions. This is how you can transform data—for example, making a string automatically uppercase.

Example: Modifying Property Behavior

function UpperCase(target: any, propertyKey: string) {
  // We create a private variable to store the value
  let value: string;

  const getter = function() {
    return value;
  };

  const setter = function(newValue: string) {
    value = newValue.toUpperCase();
  };

  // Redefine the property on the class prototype
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Example {
  @UpperCase
  name: string;
}

const example = new Example();
example.name = "hello world";
console.log(example.name);  // Output: HELLO WORLD
Best Practice: When using Object.defineProperty in a decorator, always set enumerable and configurable to true unless you have a specific reason to hide or lock the property.

 

Property Decorators with Parameters

Sometimes you need to pass data into your decorator. To do this, you use a Decorator Factory. This is a function that returns the actual decorator function.

Example: Property Decorator with Parameters

function MaxLength(limit: number) {
  return function(target: any, propertyKey: string) {
    let value: string;

    const getter = () => value;
    const setter = (newValue: string) => {
      if (newValue.length > limit) {
        console.error(`Error: ${propertyKey} cannot be longer than ${limit} chars.`);
      } else {
        value = newValue;
      }
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter
    });
  };
}

class UserProfile {
  @MaxLength(10)
  username: string;
}

const user = new UserProfile();
user.username = "TypescriptMaster"; // Output: Error: username cannot be longer than 10 chars.
  • This pattern is widely used in frameworks like NestJS or TypeORM for configuration.

 

Using Property Decorators for Validation

Validation is one of the most practical use cases for decorators. You can prevent invalid data from ever reaching your class instances by throwing errors in the setter.

Example: Property Decorator for Validation

function Required(target: any, propertyKey: string) {
  let value: any;

  const getter = () => value;
  const setter = (newValue: any) => {
    if (newValue === null || newValue === undefined) {
      throw new Error(`Property ${propertyKey} is required and cannot be null.`);
    }
    value = newValue;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter
  });
}

class Product {
  @Required
  title: string;
}

const p = new Product();
try {
  p.title = null; // Throws error
} catch (e) {
  console.log(e.message);
}

 

Using Property Decorators with Accessors

While we usually apply decorators to simple properties, they can also be applied to Accessors (getters and setters). Note that TypeScript only allows you to decorate either the getter or the setter for a single property name, not both.

Example: Property Decorators with Accessors

function ReadOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.writable = false;
}

class Configuration {
  private _apiKey: string = "12345-ABCDE";

  @ReadOnly
  get apiKey() {
    return this._apiKey;
  }
}
  • When applied to an accessor, the decorator receives a third argument: the Property Descriptor, allowing you to easily toggle settings like writable.

 

Summary

  • Property decorators provide a declarative way to intercept, validate, or transform class data.
  • They take two arguments: the target (prototype) and the propertyKey.
  • To modify values, you typically use Object.defineProperty to create custom getters and setters.
  • Decorator Factories allow you to pass custom arguments into your decorators for flexible configurations.
  • They are essential tools for building modern, scalable TypeScript applications and are heavily used in major frameworks.