Introduction to Classes in TypeScript

In modern web development, organizing code is just as important as writing it. Classes in TypeScript act as blueprints for creating objects, allowing you to group data (properties) and behavior (methods) into a single, cohesive unit. While JavaScript introduced classes in ES6, TypeScript takes them to the next level by adding static typing. This means you catch errors during development rather than when your users are running the app, making your codebase more predictable and easier to scale.

Developer Tip: Think of a Class as a cookie cutter and the Objects as the cookies. The cutter defines the shape, but each cookie can have its own individual decorations.

 

Creating a Basic Class

To define a class in TypeScript, you use the class keyword. Unlike plain JavaScript, TypeScript requires you to declare the types of your properties upfront. This allows the compiler to ensure that every instance of your class follows the correct structure.

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): string {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

const person1 = new Person("Alice", 30);
console.log(person1.greet());  // Output: Hello, my name is Alice and I am 30 years old.

In this example:

  • Properties: name and age are explicitly typed so we don't accidentally assign a number to the name.
  • The Constructor: This is a special function that runs once when you create a new instance using the new keyword.
  • Methods: greet() is a function defined inside the class that has access to the instance's data via the this keyword.
Best Practice: Always initialize your properties. If a property isn't assigned in the constructor or at declaration, TypeScript will flag it as an error unless you mark it as optional.

Class Constructor

The constructor's primary job is to set up the initial state of your object. In TypeScript, you can even use "Parameter Properties" to shorten your code, though we'll stick to the standard way for now to keep things clear.

class Car {
  make: string;
  model: string;

  constructor(make: string, model: string) {
    this.make = make;
    this.model = model;
  }

  getDetails(): string {
    return `${this.make} ${this.model}`;
  }
}

const myCar = new Car("Toyota", "Corolla");
console.log(myCar.getDetails());  // Output: Toyota Corolla
Common Mistake: Forgetting to use this when referencing class properties. Inside a class, make refers to a local variable, while this.make refers to the class property.

Access Modifiers

One of TypeScript's most powerful features is the ability to control who can see or change your data. We use Access Modifiers to protect the internal state of our objects:

  • public: The default. Anyone can see and change this property from outside the class.
  • private: Only code inside this specific class can see or change this. It's hidden from the rest of the app.
  • protected: Similar to private, but classes that inherit from this one (subclasses) can also access it.
class Employee {
  private id: number;
  public name: string;
  protected position: string;

  constructor(id: number, name: string, position: string) {
    this.id = id;
    this.name = name;
    this.position = position;
  }

  getEmployeeDetails(): string {
    return `ID: ${this.id}, Name: ${this.name}, Position: ${this.position}`;
  }
}

const emp = new Employee(1, "John", "Manager");
console.log(emp.name); // Works fine: John
// console.log(emp.id);  // Error: 'id' is private and only accessible within class 'Employee'.
Watch Out: TypeScript's private modifier is a "soft" privacy check that happens during compilation. Once the code is turned into JavaScript, the property is technically accessible unless you use the newer ECMAScript private fields (e.g., #id).

Inheritance in Classes

Inheritance allows you to create a specialized version of a class without rewriting all the logic. You use the extends keyword to build a "child" class based on a "parent" class. This is perfect for sharing common logic (like "Animal" traits) while allowing for specific behaviors (like "Dog" barking).

class Animal {
  name: string;

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

  makeSound(): string {
    return `${this.name} makes a sound.`;
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    super(name);  // This triggers the Animal constructor
    this.breed = breed;
  }

  // We are "overriding" the parent method with a specific version
  makeSound(): string {
    return `${this.name} barks.`;
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
console.log(dog.makeSound());  // Output: Buddy barks.
Developer Tip: When using inheritance, you must call super() in the child constructor before you try to use this. If you don't, TypeScript will throw an error.

Getters and Setters

Sometimes you want to add logic when someone reads or writes to a property for example, validating a price or formatting a name. Getters and setters let you define methods that look like properties to the outside world but run logic behind the scenes.

class BankAccount {
  private _balance: number;

  constructor(initialBalance: number) {
    this._balance = initialBalance;
  }

  // This acts like a property
  get balance(): number {
    return this._balance;
  }

  // This adds validation before changing the value
  set deposit(amount: number) {
    if (amount > 0) {
      this._balance += amount;
    } else {
      console.error("Deposit must be positive!");
    }
  }
}

const account = new BankAccount(1000);
console.log(account.balance);  // Accessing like a property
account.deposit = 500;         // Setting like a property
Best Practice: Use getters and setters to encapsulate your data. This prevents other parts of your app from setting invalid values (like a negative bank balance).

Static Methods and Properties

Most class members belong to the "instance" (each individual object). However, static members belong to the class itself. You don't need to use the new keyword to access them. They are perfect for utility functions or global configurations.

class MathUtils {
  static PI: number = 3.14159;

  static calculateCircleArea(radius: number): number {
    return this.PI * radius * radius;
  }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(10)); // 314.159

In this example, we don't need to create a new MathUtils() because the methods don't rely on any specific instance data.

 

Summary

Classes in TypeScript are a foundational tool for any developer looking to write clean, organized, and scalable code. By using Access Modifiers, you can protect your data; with Inheritance, you can reuse logic; and with Types, you can ensure your objects always behave exactly as expected. Mastering classes is a major step toward becoming an expert TypeScript developer and building complex applications with confidence.