Learn TypeScript With Me in 14 Days Day 6

Posted on 6 October 2024 Reading time: 6 min read
Learn TypeScript With Me in 14 Days Day 6

Day 6 - Classes in TypeScript

Today, we will look into Classes in TypeScript, which allow us to implement object-oriented programming (OOP) principles in a type-safe way. We’ll cover how to define classes, constructors, properties, methods, and also explore getters and setters for more control over how data is accessed and updated. By the end of this session, we will have refactored our Task Manager project using these concepts.

Why Use Classes in TypeScript?

Classes allow you to structure your code using the object-oriented programming (OOP) paradigm. In TypeScript, classes are enhanced with static typing, making them more robust than plain JavaScript classes. They allow you to:

  1. Encapsulate data and logic within objects.
  2. Create reusable blueprints (classes) for objects, such as tasks in our Task Manager.
  3. Inherit and extend functionality via inheritance, making your code more modular.

Step 1: Defining a Simple Class

Let’s start by defining a simple class for Task in TypeScript.

class Task {
  id: number;
  title: string;
  completed: boolean;

  constructor(id: number, title: string, completed: boolean = false) {
    this.id = id;
    this.title = title;
    this.completed = completed;
  }

  toggleCompletion(): void {
    this.completed = !this.completed;
  }
}

const task = new Task(1, 'Learn TypeScript');
console.log(task);

Explanation:

  • Properties: We define three properties: id, title, and completed.
  • Constructor: The constructor function initializes the properties when a new instance of the class is created.
  • Method: The toggleCompletion method is defined to mark a task as completed or incomplete.

Step 2: Getters and Setters in TypeScript

Getters and setters provide a controlled way to access and modify properties of a class. Instead of directly accessing or modifying properties, you can use getters to retrieve values and setters to update them with additional logic or validation.

Example of Getters and Setters:

Let’s add a getter and setter for the title property, allowing us to retrieve and set the task title safely.

class Task {
  private id: number;
  private _title: string;
  private completed: boolean;

  constructor(id: number, title: string, completed: boolean = false) {
    this.id = id;
    this._title = title;
    this.completed = completed;
  }

  // Getter for title
  get title(): string {
    return this._title;
  }

  // Setter for title with validation
  set title(newTitle: string) {
    if (newTitle.length > 0) {
      this._title = newTitle;
    } else {
      console.log('Title cannot be empty.');
    }
  }

  toggleCompletion(): void {
    this.completed = !this.completed;
  }

  getStatus(): string {
    return this.completed ? 'Completed' : 'Incomplete';
  }
}

const task = new Task(1, 'Learn TypeScript');
console.log(task.title); // Output: Learn TypeScript

task.title = ''; // Output: Title cannot be empty.
task.title = 'Master TypeScript';
console.log(task.title); // Output: Master TypeScript

Explanation:

  • Getter (get title()): This allows you to retrieve the task title as if it were a regular property (task.title), but it’s controlled by the getter.
  • Setter (set title(newTitle: string)): The setter allows you to set a new value for the title property, but we’ve added validation to ensure the title isn’t empty. If an empty string is passed, it prints an error message instead of updating the title.

By using getters and setters, you gain fine-grained control over how your class properties are accessed and modified.

Step 3: Refactoring the Task Manager with Classes, Getters, and Setters

Let’s refactor our Task Manager project to use the Task class with getters and setters for improved control.

1. Define the Task Class in task.ts with Getters and Setters

class Task {
  private static idCounter = 0;
  private id: number;
  private _title: string;
  private completed: boolean;
  dueDate?: Date;

  constructor(title: string, dueDate?: Date) {
    Task.idCounter++;
    this.id = Task.idCounter;
    this._title = title;
    this.completed = false;
    this.dueDate = dueDate;
  }

  // Getter for title
  get title(): string {
    return this._title;
  }

  // Setter for title with validation
  set title(newTitle: string) {
    if (newTitle.length > 0) {
      this._title = newTitle;
    } else {
      console.log('Title cannot be empty.');
    }
  }

  toggleCompletion(): void {
    this.completed = !this.completed;
  }

  getDetails(): string {
    return `Task: ${this.title} | Status: ${this.completed ? 'Completed' : 'Incomplete'}`;
  }
}

export { Task };

2. Refactor the Task Creation Logic in index.ts

import { Task } from './task';

let tasks: Task[] = [];

function addTask(title: string, dueDate?: Date): void {
  const newTask = new Task(title, dueDate);
  tasks.push(newTask);
}

function toggleTaskCompletion(id: number): void {
  const task = tasks.find(t => t['id'] === id);
  if (task) {
    task.toggleCompletion();
  }
}

function updateTaskTitle(id: number, newTitle: string): void {
  const task = tasks.find(t => t['id'] === id);
  if (task) {
    task.title = newTitle; // Using the setter to update the title
  }
}

addTask('Learn TypeScript');
addTask('Build Task Manager', new Date('2024-12-01'));

console.log(tasks.map(task => task.getDetails())); // Get details of all tasks
updateTaskTitle(1, 'Master TypeScript');
console.log(tasks.map(task => task.getDetails())); // Check if title was updated

Explanation:

  • We used a getter to access the task title, and a setter to update the title with validation.
  • We added a new function updateTaskTitle() to update the task’s title using the setter.
  • This refactor improves code maintainability and ensures that task titles cannot be set to empty values.

Step 4: Inheritance in TypeScript Classes

TypeScript supports class inheritance, allowing one class to extend another, inheriting its properties and methods. Let’s create a RecurringTask class that extends Task.

Example of Inheritance:

class RecurringTask extends Task {
  recurrenceInterval: string;

  constructor(title: string, recurrenceInterval: string, dueDate?: Date) {
    super(title, dueDate);
    this.recurrenceInterval = recurrenceInterval;
  }

  getDetails(): string {
    return `${super.getDetails()} | Recurs: ${this.recurrenceInterval}`;
  }
}

const recurringTask = new RecurringTask('Weekly Meeting', 'Weekly');
console.log(recurringTask.getDetails()); // Output includes recurrence information

Explanation:

  • extends keyword: The RecurringTask class inherits from Task, adding a new property: recurrenceInterval.
  • Overriding Methods: We override the getDetails() method to include recurrence information.

Step 5: Applying OOP Principles to Task Manager

With classes, getters, setters, and inheritance, you can now structure the Task Manager project using OOP principles:

  1. Encapsulation: Keep properties like id private, exposing only necessary methods.
  2. Reusability: Reuse task-related functionality by creating a parent Task class and extending it for specific types of tasks.
  3. Inheritance: Use inheritance to add more features (e.g., RecurringTask).
  4. Getters and Setters: Control how data is accessed and modified by using getters and setters to encapsulate logic and validation.

What’s Next?

Today, we have learnt how to use classes in TypeScript to structure our code using object-oriented principles. We explored class inheritance, encapsulation, and the benefits of using classes for organising our code.

Tomorrow, we’ll explore Generics in TypeScript—a powerful feature that allows you to write flexible and reusable components and functions. If you have any questions or comments, feel free contact me at steve [at] stevepopoola.uk.

Useful Resources:

TypeScript Handbook - Classes