How to use getters and setters in TypeScript

Jul 09, 2023#typescript#how-to

In TypeScript, you can use getter and setter methods to provide controlled access to class properties. Getter methods allow you to retrieve the value of a property, while setter methods enable you to modify the value of a property with certain validations or actions. Here’s an example:

class MyClass {
  private _myProperty: string;

  get myProperty(): string {
    return this._myProperty;
  }

  set myProperty(value: string) {
    // Add any validation or additional logic here
    this._myProperty = value;
  }
}

const myObject = new MyClass();

// Using the getter
console.log(myObject.myProperty); // Output: undefined

// Using the setter
myObject.myProperty = "Hello, world!";

// Using the getter again
console.log(myObject.myProperty); // Output: Hello, world!

In the example above, we have a class MyClass with a private property _myProperty. We define a getter method myProperty() that returns the value of _myProperty and a setter method myProperty(value: string) that sets the value of _myProperty.

Within the setter method, you can add any additional logic or validation to ensure the assigned value meets specific criteria. For example, you could check if the value is a non-empty string or falls within a certain range before assigning it.

When you access myProperty on an instance of MyClass, the getter or setter method will be invoked automatically depending on whether you’re retrieving or assigning a value.

You can use access modifiers (public, private, or protected) with getter and setter methods to control their accessibility from outside the class. This allows you to encapsulate and protect class properties while providing controlled access through getters and setters.

Note that it’s common practice in TypeScript to prefix private properties with an underscore (_) to distinguish them from public properties and prevent naming conflicts with the getter and setter methods.

strictPropertyInitialization

The strictPropertyInitialization compiler option is used to enforce stricter initialization rules for class properties. When strictPropertyInitialization is enabled, TypeScript requires that all non-optional properties of a class are initialized either in the constructor or at the point of declaration.

{
  "compilerOptions": {
    // Default to `true` if strict, `false` otherwise.
    "strictPropertyInitialization": false,
  }
}

In case strictPropertyInitialization is true, compiler will complain:

class MyClass {
  private _myProperty: string; // ❌ Property '_myProperty' has no initializer and is not definitely assigned in the constructor.

  get myProperty(): string {
    return this._myProperty;
  }

  set myProperty(value: string) {
    // Add any validation or additional logic here
    this._myProperty = value;
  }
}

Read-only properties

To create a read-only property, you can use the readonly modifier in front of the property declaration. Read-only properties can only be assigned a value during object creation or within the class constructor.

class MyClass {
  readonly readOnlyProperty: string;

  constructor(value: string) {
    this.readOnlyProperty = value;
  }
}

const myObject = new MyClass("Hello");

console.log(myObject.readOnlyProperty); // Output: Hello
myObject.readOnlyProperty = "World"; // Error: Cannot assign to 'readOnlyProperty' because it is a read-only property.

The readonly modifier is only checked by the compiler at compile time. It does not prevent mutations at runtime. If you want to make an object immutable at runtime, you can use Object.freeze().

You can use getters without setters to define properties that can only be read from outside the class. For example:

class MyClass {
  private _readOnlyProperty: string = "Hello";

  get readOnlyProperty(): string {
    return this._readOnlyProperty;
  }
}

const myObject = new MyClass();

console.log(myObject.readOnlyProperty); // Output: Hello
myObject.readOnlyProperty = "World"; // Error: Cannot assign to 'readOnlyProperty' because it is a read-only property.

However, using getters only does not prevent you from changing the private property _readOnlyProperty inside the class. If you want to make it truly immutable, you can use the readonly modifier on it.

Computed properties

Getters can be used to compute and return a value based on other class properties or perform any custom logic. This can be helpful when you want to derive a value dynamically.

class Circle {
  private _radius: number;

  constructor(radius: number) {
    this._radius = radius;
  }

  get diameter(): number {
    return this._radius * 2;
  }

  set diameter(value: number) {
    this._radius = value / 2;
  }
}

const myCircle = new Circle(5);
console.log(myCircle.diameter); // Output: 10
myCircle.diameter = 16;
console.log(myCircle.diameter); // Output: 16

In the above example, the diameter getter returns the computed diameter based on the _radius property, and the diameter setter updates the _radius based on the new diameter value.

Difference between a getter and a method

A getter is a special kind of method that returns the value of a property. A method is a function that performs some action or calculation. The main difference between a getter and a method is how you call them. A getter is called like a property, without parentheses, while a method is called like a function, with parentheses. For example:

class Circle {
  private _radius: number;

  constructor(radius: number) {
    this._radius = radius;
  }

  // this is a getter
  public get area(): number {
    return Math.PI * this._radius * this._radius;
  }

  // this is a method
  public changeRadius(newRadius: number) {
    this._radius = newRadius;
  }
}

let circle = new Circle(10);
console.log(circle.area); // calls the getter, returns 314.1592653589793
circle.changeRadius(20); // calls the method, changes the radius to 20
console.log(circle.area); // calls the getter again, returns 1256.6370614359173

Another difference between a getter and a method is that a getter should not have any side effects, while a method can. A side effect is any change in the state of the object or the environment that is not related to the return value.

For example, changing the value of a property, logging something to the console, or making an API call are all side effects. A getter should only return a value based on the current state of the object, without modifying it or anything else. A method can do anything it wants, as long as it is clear from its name and documentation. For example:

class Counter {
  private _count: number;

  constructor() {
    this._count = 0;
  }

  // this is a getter
  public get count(): number {
    return this._count;
  }

  // this is a method
  public increment() {
    this._count++; // this is a side effect
    console.log("Count increased"); // this is another side effect
  }
}

let counter = new Counter();
console.log(counter.count); // calls the getter, returns 0
counter.increment(); // calls the method, changes the count to 1 and logs "Count increased"
console.log(counter.count); // calls the getter again, returns 1