Property Descriptor in TypeScript: A Complete Guide

Updated: January 8, 2024 By: Guest Contributor Post a comment

Overview

TypeScript, an extension of JavaScript, provides robust typing capabilities to design-time checks and autocompletion. A key TypeScript feature is the ability to define property descriptors within classes, giving developers fine-grained control over how properties behave. This guide dives into using property descriptors to empower your TypeScript development.

Understanding Property Descriptors

In TypeScript, property descriptors are an advanced feature that allows you to define the characteristics of object properties. A descriptor gives you control over property attributes such as enumerability, configurability, writability, and value specifics, granting you power that JavaScript could only dream of in the past.

class Person {
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  get name() {
    return this._name;
  }

  set name(newName: string) {
    this._name = newName;
  }
}

Defining Property Descriptors

To harness property descriptors, you use the Object.defineProperty method. Let’s apply a descriptor to a property to understand the mechanics behind it.

class Person {
  private _age: number;

  constructor(age: number) {
    Object.defineProperty(this, 'age', {
      get: () => this._age,
      set: (newAge) => {
        if(newAge >= 0) {
          this._age = newAge;
        } else {
          throw new Error('Age must be a positive number');
        }
      },
      enumerable: true,
      configurable: true
    });

    this._age = age;
  }
}

Describing Properties with Decorators

TypeScript’s experimental decorators allow for metadata-driven design and provide a declarative way to modify property descriptors. The following example showcases a custom decorator that sets up a descriptor.

function ReadOnly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false
  });
}

class Secret {
  @ReadOnly
  code: string = 'ABC123';
}

const secretInstance = new Secret();
secretInstance.code = 'DEF456'; // Error: Cannot assign to 'code' because it is a read-only property

Advanced Property Descriptor Patterns

With TypeScript, you can employ more intricate property descriptor patterns, enhancing encapsulation and design. Consider the example of aggregating property decorators to compose complex property behaviors.

function Enumerable(value: boolean) {
  return function(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      enumerable: value
    });
  };
}

function Configurable(value: boolean) {
  return function(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      configurable: value
    });
  };
}

class Example {
  @Enumerable(false)
  @Configurable(false)
  id: string;

  constructor(id: string) {
    this.id = id;
  }
}

Utilizing Reflect Metadata

TypeScript and the Reflect.metadata proposal can further enhance property descriptors, particularly when you use TypeScript with runtimes that support decorators fully, like Angular or NestJS.

import 'reflect-metadata';

const FormatMetadataKey = Symbol('format');

function Format(format: string) {
  return Reflect.metadata(FormatMetadataKey, format);
}

function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(FormatMetadataKey, target, propertyKey);
}

class Greeter {
  @Format('Hello, %s')
  greeting: string;
}

console.log(getFormat(new Greeter(), 'greeting')); // Output: Hello, %s

Conclusion

To encapsulate, TypeScript’s property descriptors provide an abundance of functionality, enabling developers to construct more precise and descriptive classes. As we’ve explored deceptively simple decorators to advanced reflective patterns, it’s evident that property descriptors and decorators propel TypeScript’s capacity for articulating clearly-defined contracts within codebases — a testament to its enterprise-grade appeal.