Skip to main content

TS Ep 19: Getters, Setters, and Asymmetric Accessors

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 19: This Article
Getters (get) and Setters (set) allow you to intercept property reads and writes in JavaScript classes. TypeScript enhances accessors by automatically enforcing readonly constraints on getter-only properties and supporting Asymmetric Accessor Types.

1. Accessor Syntax & Encapsulation
#

Accessors look like methods in their declaration syntax, but they are consumed like standard object properties without appending function invocation parentheses ().

class BankAccount {
  // Private backing field (by convention prefixed with '_')
  private _balance: number = 0;

  constructor(initialBalance: number) {
    this.balance = initialBalance; // Routes through setter validation!
  }

  // Getter: Invoked when reading account.balance
  get balance(): number {
    return this._balance;
  }

  // Setter: Invoked when writing account.balance = 500
  set balance(newBalance: number) {
    if (newBalance < 0) {
      throw new Error("[Validation Error]: Account balance cannot be negative.");
    }
    this._balance = newBalance;
  }
}

const account = new BankAccount(100);

// Reading via Getter (No parentheses!)
console.log(account.balance); // 100

// Writing via Setter
account.balance = 250;
console.log(account.balance); // 250

// ❌ Throws Runtime Exception: [Validation Error]: Account balance cannot be negative.
// account.balance = -50;

2. Computed Read-Only Properties (Getter Only)
#

If a class defines a get accessor without a corresponding set accessor, TypeScript automatically infers the property as readonly. Any attempt to assign a value to a getter-only property will result in a compile-time type error.

class Circle {
  constructor(public radius: number) {}

  // Computed property getter (No setter provided)
  get area(): number {
    return Math.PI * (this.radius ** 2);
  }

  get circumference(): number {
    return 2 * Math.PI * this.radius;
  }
}

const circle = new Circle(5);
console.log(circle.area); // ~78.54

// ❌ Compiler Error: Cannot assign to 'area' because it is a read-only property.
// circle.area = 100;

3. Backing Field Patterns: _field vs #field
#

Because an accessor cannot share the exact same identifier as its backing storage property (e.g. get name() cannot read this.name, as that triggers infinite recursion), you must store the raw data in a distinct field.

Pattern A: Conventional Prefix (_field)
#

class UserA {
  private _email: string = "";

  get email(): string { return this._email; }
  set email(val: string) { this._email = val; }
}

Pattern B: Native ES2022 Private Backing Field (#field)
#

class UserB {
  // Uses native hard privacy for backing storage
  #email: string = "";

  get email(): string { 
    return this.#email; 
  }
  set email(val: string) { 
    if (!val.includes("@")) throw new Error("Invalid email format");
    this.#email = val; 
  }
}

4. Asymmetric Accessor Types (TypeScript 4.3+)
#

Prior to TypeScript 4.3, a setter was required to have the exact same type as its getter.

Since TypeScript 4.3, you can declare Asymmetric Accessor Types, where the setter accepts a broader type than the getter returns. This is ideal for parsing string inputs or timestamp numbers into structured domain objects!

class ScheduleEvent {
  private _eventDate: Date = new Date();

  // 1. Getter returns a strict, guaranteed Date object
  get eventDate(): Date {
    return this._eventDate;
  }

  // 2. Setter accepts Date, ISO string, or Unix epoch timestamp number!
  set eventDate(value: Date | string | number) {
    if (value instanceof Date) {
      this._eventDate = value;
    } else if (typeof value === "string") {
      this._eventDate = new Date(value);
    } else if (typeof value === "number") {
      this._eventDate = new Date(value);
    } else {
      throw new Error("Invalid date input type.");
    }
  }
}

const event = new ScheduleEvent();

// Assigning a string to the setter:
event.eventDate = "2026-12-31T23:59:59Z"; 

// Assigning a Unix timestamp number to the setter:
event.eventDate = 1700000000000;

// The getter strictly returns a Date instance with date methods:
const year: number = event.eventDate.getFullYear();
console.log(`Event Year: ${year}`);

Type Signature of Asymmetric Accessors
#

When inspecting eventDate on ScheduleEvent:

  • Read Type: Date
  • Write Type: Date | string | number

This provides maximum flexibility for consumers writing data, while guaranteeing strict, predictable types when reading data!


5. Compiler Target Requirements
#

To compile getters and setters, your tsconfig.json target must be set to ES5 or higher:

{
  "compilerOptions": {
    "target": "ES2022" // Required for Object.defineProperty / accessor emission
  }
}

If target is set to ES3, tsc will throw an error because ES3 JavaScript engines did not support property accessors.


Summary & Next Steps
#

In this episode:

  • We implemented class encapsulation using get and set accessors.
  • We proved that getter-only properties are automatically inferred as readonly.
  • We compared _field soft private vs #field hard private backing storage.
  • We built flexible input parsers using Asymmetric Accessor Types (get Date / set Date | string | number).

In Episode 20: Implementing Interfaces (implements), we will explore how classes enforce architectural contracts using interface implementation!

typescript - This article is part of a series.
Part 19: This Article