1. javascript
  2. /typescript
  3. /classes

TypeScript Classes

TypeScript enhances JavaScript's class syntax with powerful features like access modifiers, abstract classes, parameter properties, and strong typing for all class members. This enables building robust object-oriented applications with clear encapsulation, inheritance hierarchies, and compile-time type safety.

Classes in TypeScript provide a blueprint for creating objects with defined properties and methods, while the type system ensures that class contracts are properly enforced throughout your application.

Basic Class Definition

TypeScript classes include type annotations for properties and methods, providing compile-time safety and better IDE support.

class User {
  // Property declarations with types
  id: number;
  name: string;
  email: string;
  isActive: boolean;

  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.isActive = true;
  }

  // Method with typed parameters and return type
  getDisplayName(): string {
    return `${this.name} (${this.email})`;
  }

  toggleStatus(): void {
    this.isActive = !this.isActive;
  }

  updateEmail(newEmail: string): boolean {
    if (newEmail.includes('@')) {
      this.email = newEmail;
      return true;
    }
    return false;
  }
}

// Creating instances
const user = new User(1, "Alice Johnson", "[email protected]");
console.log(user.getDisplayName()); // "Alice Johnson ([email protected])"

Access Modifiers

TypeScript provides three access modifiers to control the visibility of class members.

class BankAccount {
  public accountNumber: string;    // Accessible everywhere
  private balance: number;         // Only accessible within this class
  protected ownerName: string;     // Accessible within this class and subclasses

  constructor(accountNumber: string, ownerName: string, initialBalance: number = 0) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  // Public method
  public getAccountInfo(): string {
    return `Account: ${this.accountNumber}, Owner: ${this.ownerName}`;
  }

  // Private method
  private validateAmount(amount: number): boolean {
    return amount > 0 && amount <= this.balance;
  }

  // Public method using private method
  public withdraw(amount: number): boolean {
    if (this.validateAmount(amount)) {
      this.balance -= amount;
      return true;
    }
    return false;
  }

  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
    }
  }

  // Protected method for subclasses
  protected getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount("12345", "John Doe", 1000);
console.log(account.accountNumber); // OK: public
// console.log(account.balance); // Error: private
// account.validateAmount(100); // Error: private method

Parameter Properties

TypeScript provides a shorthand syntax for creating and initializing class properties through constructor parameters.

class Product {
  // Traditional approach
  /* 
  private id: number;
  public name: string;
  protected price: number;

  constructor(id: number, name: string, price: number) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
  */

  // Parameter properties shorthand
  constructor(
    private id: number,
    public name: string,
    protected price: number,
    public readonly category: string
  ) {
    // Properties are automatically created and initialized
  }

  public getInfo(): string {
    return `${this.name} - $${this.price}`;
  }

  protected getId(): number {
    return this.id;
  }
}

const laptop = new Product(1, "MacBook Pro", 2499, "Electronics");
console.log(laptop.name); // OK: public
console.log(laptop.category); // OK: readonly public
// laptop.category = "New Category"; // Error: readonly
// console.log(laptop.id); // Error: private

Inheritance

Classes can extend other classes, inheriting their properties and methods while adding new functionality.

class Animal {
  protected name: string;
  protected age: number;

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

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

  public getAge(): number {
    return this.age;
  }
}

class Dog extends Animal {
  private breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age); // Call parent constructor
    this.breed = breed;
  }

  // Override parent method
  public speak(): string {
    return `${this.name} barks loudly!`;
  }

  // Add new method
  public getBreed(): string {
    return this.breed;
  }

  // Method using protected property from parent
  public introduce(): string {
    return `This is ${this.name}, a ${this.age}-year-old ${this.breed}`;
  }
}

class Cat extends Animal {
  private isIndoor: boolean;

  constructor(name: string, age: number, isIndoor: boolean = true) {
    super(name, age);
    this.isIndoor = isIndoor;
  }

  public speak(): string {
    return `${this.name} meows softly`;
  }

  public isIndoorCat(): boolean {
    return this.isIndoor;
  }
}

const dog = new Dog("Buddy", 3, "Golden Retriever");
const cat = new Cat("Whiskers", 2, false);

console.log(dog.speak()); // "Buddy barks loudly!"
console.log(cat.speak()); // "Whiskers meows softly"
console.log(dog.introduce()); // "This is Buddy, a 3-year-old Golden Retriever"

Abstract Classes

Abstract classes cannot be instantiated directly and serve as base classes for other classes.

abstract class Shape {
  protected name: string;

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

  // Abstract method - must be implemented by subclasses
  abstract getArea(): number;
  abstract getPerimeter(): number;

  // Concrete method - available to all subclasses
  public displayInfo(): string {
    return `${this.name}: Area = ${this.getArea()}, Perimeter = ${this.getPerimeter()}`;
  }

  // Protected method for subclasses
  protected getName(): string {
    return this.name;
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super("Rectangle");
  }

  getArea(): number {
    return this.width * this.height;
  }

  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }

  public getDimensions(): string {
    return `${this.width} x ${this.height}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super("Circle");
  }

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

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

  public getRadius(): number {
    return this.radius;
  }
}

// const shape = new Shape("test"); // Error: Cannot instantiate abstract class
const rectangle = new Rectangle(10, 5);
const circle = new Circle(7);

console.log(rectangle.displayInfo()); // Rectangle: Area = 50, Perimeter = 30
console.log(circle.displayInfo()); // Circle: Area = 153.94, Perimeter = 43.98

Static Members

Static properties and methods belong to the class itself rather than to instances.

class MathUtilities {
  static readonly PI: number = 3.14159;
  static readonly E: number = 2.71828;

  private static instanceCount: number = 0;

  constructor() {
    MathUtilities.instanceCount++;
  }

  static add(a: number, b: number): number {
    return a + b;
  }

  static multiply(a: number, b: number): number {
    return a * b;
  }

  static getCircleArea(radius: number): number {
    return MathUtilities.PI * radius ** 2;
  }

  static getInstanceCount(): number {
    return MathUtilities.instanceCount;
  }

  // Instance method accessing static member
  getPI(): number {
    return MathUtilities.PI;
  }
}

// Using static members without instantiation
console.log(MathUtilities.add(5, 3)); // 8
console.log(MathUtilities.getCircleArea(5)); // 78.54
console.log(MathUtilities.PI); // 3.14159

// Creating instances affects static counter
const math1 = new MathUtilities();
const math2 = new MathUtilities();
console.log(MathUtilities.getInstanceCount()); // 2

Getters and Setters

Accessors provide controlled access to class properties with validation and computed values.

class Temperature {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  // Getter for celsius
  get celsius(): number {
    return this._celsius;
  }

  // Setter for celsius with validation
  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature cannot be below absolute zero");
    }
    this._celsius = value;
  }

  // Computed property - fahrenheit
  get fahrenheit(): number {
    return (this._celsius * 9/5) + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5/9;
  }

  // Computed property - kelvin
  get kelvin(): number {
    return this._celsius + 273.15;
  }

  set kelvin(value: number) {
    this.celsius = value - 273.15;
  }

  // Read-only computed property
  get description(): string {
    if (this._celsius < 0) return "Below freezing";
    if (this._celsius < 10) return "Cold";
    if (this._celsius < 25) return "Cool";
    if (this._celsius < 35) return "Warm";
    return "Hot";
  }
}

const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
console.log(temp.description); // "Warm"

temp.fahrenheit = 86;
console.log(temp.celsius); // 30

Generic Classes

Classes can be generic, allowing them to work with different types while maintaining type safety.

class Container<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  remove(index: number): T | undefined {
    return this.items.splice(index, 1)[0];
  }

  get(index: number): T | undefined {
    return this.items[index];
  }

  getAll(): T[] {
    return [...this.items];
  }

  find(predicate: (item: T) => boolean): T | undefined {
    return this.items.find(predicate);
  }

  get length(): number {
    return this.items.length;
  }
}

// Usage with different types
const stringContainer = new Container<string>();
stringContainer.add("hello");
stringContainer.add("world");
console.log(stringContainer.get(0)); // "hello"

const numberContainer = new Container<number>();
numberContainer.add(1);
numberContainer.add(2);
numberContainer.add(3);
console.log(numberContainer.getAll()); // [1, 2, 3]

interface User {
  id: number;
  name: string;
}

const userContainer = new Container<User>();
userContainer.add({ id: 1, name: "Alice" });
userContainer.add({ id: 2, name: "Bob" });
const alice = userContainer.find(user => user.name === "Alice");

Class Implementing Interfaces

Classes can implement interfaces to ensure they conform to specific contracts.

interface Drawable {
  draw(): void;
}

interface Movable {
  x: number;
  y: number;
  move(dx: number, dy: number): void;
}

interface Resizable {
  width: number;
  height: number;
  resize(width: number, height: number): void;
}

class GameObject implements Drawable, Movable {
  constructor(public x: number, public y: number) {}

  draw(): void {
    console.log(`Drawing at (${this.x}, ${this.y})`);
  }

  move(dx: number, dy: number): void {
    this.x += dx;
    this.y += dy;
  }
}

class Rectangle implements Drawable, Movable, Resizable {
  constructor(
    public x: number,
    public y: number,
    public width: number,
    public height: number
  ) {}

  draw(): void {
    console.log(`Drawing rectangle at (${this.x}, ${this.y}) with size ${this.width}x${this.height}`);
  }

  move(dx: number, dy: number): void {
    this.x += dx;
    this.y += dy;
  }

  resize(width: number, height: number): void {
    this.width = width;
    this.height = height;
  }
}

const gameObject = new GameObject(10, 20);
const rectangle = new Rectangle(0, 0, 100, 50);

gameObject.draw();
rectangle.draw();
rectangle.resize(200, 100);

TypeScript classes provide a robust foundation for object-oriented programming with strong typing, encapsulation, and inheritance. They enable building maintainable applications with clear contracts and compile-time safety while leveraging familiar object-oriented patterns.