Mastering OOP with TypeScript: An In-Depth Look at Encapsulation

Object-Oriented Programming (OOP) is a popular paradigm that's been widely used for decades. It is a powerful tool that allows us developers to model real-world objects and their interactions with each other. OOP helps organize and structure code in a way that is easy to understand, maintain and extend.

When combined with a type system like TypeScript, OOP becomes even more powerful. TypeScript provides a way to ensure that the code is correct and easy to reason about. It also helps to prevent common mistakes that can occur when working with OOP.

One of the most important concepts in OOP is encapsulation. Encapsulation is the practice of hiding the internal details of an object from the outside world. With it, we can change the implementation of an object without affecting the code that uses it.

Using Encapsulation in TypeScript

Take this Duration class as an example:

export interface DurationDto {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
}

export type NewDurationDto = Partial<Duration>;

export class Duration {
  private _days: number;
  private _hours: number;
  private _minutes: number;
  private _seconds: number;

  constructor(props: NewDurationDto) {
    this.days = props.days;
    this.hours = props.hours;
    this.minutes = props.minutes;
    this.seconds = props.seconds;
  }

  static parse(value: string): Result<Duration> {
    try {
      const [ days, rest ] = value.split(".");
      const [ hours, minutes, seconds ] = rest.split(":");
      return Result.success(new Duration({
        days: Number(days),
        hours: Number(hours),
        minutes: Number(minutes),
        seconds: Number(seconds),
      }));
    }
    catch {
      return Result.failure<Duration>(`Invalid duration: "${value}". Must be in the format "d.hh:mm:ss".`);
    }
  }

  static fromMinutes(value: number): Duration {
    const days = Math.floor(value / 1440);
    const hours = Math.floor(value / 60) - days * 24;
    const minutes = value - days * 1440 - hours * 60;
    return new Duration({ days, hours, minutes });
  }

  get days(): number {
    return this._days;
  }
  set days(value: number | nothing) {
    this._days = guard(value, "days").default(0).min(0).integer().value;
  }

  get hours(): number {
    return this._hours;
  }
  set hours(value: number | nothing) {
    this._hours = guard(value, "hours").default(0).range(0, 23).integer().value;
  }

  get minutes(): number {
    return this._minutes;
  }
  set minutes(value: number | nothing) {
    this._minutes = guard(value, "minutes").default(0).range(0, 59).integer().value;
  }

  get seconds(): number {
    return this._seconds;
  }
  set seconds(value: number | nothing) {
    this._seconds = guard(value, "seconds").default(0).range(0, 59).integer().value;
  }

  get totalHours(): number {
    return this.days * 24 + this.hours;
  }

  get totalMinutes(): number {
    return this.totalHours * 60 + this.minutes;
  }

  get totalSeconds(): number {
    return this.totalMinutes * 60 + this.seconds;
  }

  toJSON(): DurationDto {
    return {
      days: this.days,
      hours: this.hours,
      minutes: this.minutes,
      seconds: this.seconds,
    };
  }

  toString(): string {
    const hours = String(this.hours).padStart(2, "0");
    const minutes = String(this.minutes).padStart(2, "0");
    const seconds = String(this.seconds).padStart(2, "0");
    return `${this.days}.${hours}:${minutes}:${seconds}`;
  }
}

This is a simple class representing a duration of time. It has properties for days, hours, minutes, and seconds. We will see how encapsulation is used to hide the internal details of the class and how TypeScript's type system is used to ensure the class is used correctly.

Private properties with getters and setters

The first thing to notice in the example code is that the properties _days, _hours, _minutes, and _seconds are marked as private. This means that they can only be accessed from within the Duration class. This is a key aspect of encapsulation. By making the properties private, we are hiding the internal details of the class from the outside world. This allows us to change the implementation of the class without affecting the code that uses it.

We can see that the properties are accessed through getters and setters. A getter is a function that is used to access the value of a property and a setter is used to set the value of a property. This allows us to add additional logic to the properties, such as validation, without affecting the code that uses the class.

The days, hours, minutes, and seconds properties are also marked with the guard function. This function is used to ensure that the values of the properties are valid. It ensures that the values are numbers, within a certain range and that they are integers. This helps to prevent common mistakes that can occur when working with the class.

Refer to the post below if you wanna learn more about the guard function used in the code:

Private setters and immutability

One way to further improve the code would be to make the setters private.

This would make the Duration class immutable, meaning that once an instance of the class is created, its properties cannot be changed.

This can have several benefits, such as making the code more predictable and reducing the likelihood of bugs caused by unexpected changes to the state of the object.

Immutable objects are often considered to be more thread-safe, as they cannot be modified by multiple threads at the same time. This is especially useful in larger projects, where multiple developers may be working on the same codebase simultaneously.

Factory methods

In addition to the private properties, there are also several static methods.

The parse method is used to parse a string representation of a duration and return a new instance of the Duration class.

The fromMinutes method is used to create a new instance of the Duration class from a number of minutes. Both of these methods return a Result object, which can be used to check if the operation was successful or not.

Depending on our use-cases, we could go ahead and add more factory methods like fromSeconds, fromHours and so on.

Serialization methods

Finally, there are also the toJSON and toString methods. These methods are used to convert the Duration class to a JSON object and a string representation respectively.

These methods are used to make it easy to work with the class in different contexts.

Conclusion

The code provided demonstrates how to effectively use OOP principles, specifically encapsulation, in TypeScript.

By keeping the internal state of the Duration class private and using getters and setters to access and modify it, we can ensure that the state of the class is always in a valid state.

Additionally, by providing static factory methods and a JSON representation, we have made the class more flexible and usable in a variety of contexts.

By following these principles, we make our code more robust and maintainable, while helping other developers make better use (or correct use, I should say) of our code.