TypeScript Classes: Concepts, Syntax, and Examples

While I’m working on a web application, I needed a way to manage users, including storing their names, emails, and roles, and updating their details as required.

Instead of writing the same code again and again for each user, I decided to use a TypeScript class. It helped me organize the code, add useful methods, and maintain a clean and reusable codebase.

In this tutorial, I will explain everything you need to know about TypeScript classes with practical examples. Whether you’re a beginner or looking to refine your skills, you’ll find valuable insights here.

What is a Class in TypeScript?

A TypeScript class is a blueprint for creating objects with pre-defined properties and methods. It follows the Object-Oriented Programming (OOP) paradigm, enabling you to implement concepts such as inheritance, encapsulation, and polymorphism.

Classes help structure your code better, making it more maintainable and scalable in large applications.

Basic TypeScript Class Syntax

Let’s start with a simple example of a TypeScript class:

class Customer {
  // Properties
  id: number;
  name: string;
  email: string;

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

  // Method
  displayInfo(): string {
    return `Customer: ${this.name}, Email: ${this.email}`;
  }
}

// Creating an instance
const customer1 = new Customer(1, "John Smith", "john@example.com");
console.log(customer1.displayInfo());  // Output: Customer: John Smith, Email: john@example.com

In this example, I’ve created a Customer class with three properties, a constructor to initialize those properties, and a method to display customer information.

TypeScript Classes

Access Modifiers in TypeScript Classes

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

1. Public

Public members are accessible from anywhere. This is the default if no modifier is specified.

class Product {
  public name: string;

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

const product = new Product("iPhone");
console.log(product.name);  // Accessible

Output:

Access Modifiers in TypeScript Classes

2. Private

Private members are only accessible within the class itself.

class BankAccount {
  private balance: number;

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

  deposit(amount: number): void {
    this.balance += amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount(1000);
// console.log(account.balance);  // Error: Property 'balance' is private
console.log(account.getBalance());  // Works: 1000

Output:

Private Access Modifiers in TypeScript Classes

3. Protected

Protected members are accessible within the class and its subclasses.

class Person {
  protected ssn: string;
  public name: string;

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

class Employee extends Person {
  private employeeId: number;

  constructor(ssn: string, name: string, id: number) {
    super(ssn, name);
    this.employeeId = id;
  }

  getSSN(): string {
    return this.ssn;  // Accessible because ssn is protected
  }
}

const emp = new Employee("123-45-6789", "Alice Johnson", 12345);
// console.log(emp.ssn);  // Error: Property 'ssn' is protected
console.log(emp.getSSN());  // Works: 123-45-6789

Output:

Protected Access Modifier in TypeScript Classes

Constructor in TypeScript Classes

TypeScript offers a concise way to define and initialize class properties in the constructor:

class Car {
  constructor(
    public make: string,
    public model: string,
    private vin: string,
    protected year: number
  ) {}

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

const myCar = new Car("Tesla", "Model 3", "5YJ3E1EA7JF000123", 2023);
console.log(myCar.getInfo());  // Output: 2023 Tesla Model 3

This shorthand syntax automatically creates and initializes the properties with the given values, saving you from writing repetitive code.

Constructor in TypeScript Classes

Implementing Interfaces in TypeScript

Classes in TypeScript can implement interfaces, ensuring they adhere to a specific contract:

interface Taxable {
  calculateTax(): number;
}

class Income implements Taxable {
  constructor(private amount: number, private taxRate: number) {}

  calculateTax(): number {
    return this.amount * this.taxRate;
  }
}

class Property implements Taxable {
  constructor(private value: number, private propertyTaxRate: number) {}

  calculateTax(): number {
    return this.value * this.propertyTaxRate;
  }
}

// Using these classes
const salary = new Income(85000, 0.24);
console.log(`Tax on income: $${salary.calculateTax()}`);  // Tax on income: $20400

const house = new Property(350000, 0.011);
console.log(`Property tax: $${house.calculateTax()}`);  // Property tax: $3850

Output:

Implement Interfaces in TypeScript

Abstract Classes in TypeScript

Abstract classes serve as base classes that cannot be instantiated directly but can be extended by other classes:

abstract class Shape {
  constructor(protected color: string) {}

  abstract calculateArea(): number;

  displayColor(): string {
    return `This shape is ${this.color}`;
  }
}

class Circle extends Shape {
  constructor(color: string, private radius: number) {
    super(color);
  }

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

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

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

// Using these classes
const circle = new Circle("red", 5);
console.log(circle.displayColor());  // This shape is red
console.log(`Area: ${circle.calculateArea().toFixed(2)} square units`);  // Area: 78.54 square units

const rectangle = new Rectangle("blue", 10, 5);
console.log(`Area: ${rectangle.calculateArea()} square units`);  // Area: 50 square units

Output:

Abstract Classes in TypeScript

Static Members in TypeScript Classes

Static properties and methods belong to the class itself, not to instances of the class:

class MathUtils {
  static PI: number = 3.14159;

  static square(x: number): number {
    return x * x;
  }

  static calculateCircumference(radius: number): number {
    return 2 * MathUtils.PI * radius;
  }
}

// Using static members without creating an instance
console.log(MathUtils.PI);  // 3.14159
console.log(MathUtils.square(4));  // 16
console.log(MathUtils.calculateCircumference(5));  // 31.4159

Output:

Static Members in TypeScript Classes

Real-World Class Example in TypeScript: E-Commerce System

Let’s create a more complex example that models part of an e-commerce system:

// Product interface
interface IProduct {
  id: number;
  name: string;
  price: number;
  getDiscountedPrice(discountPercent: number): number;
}

// Abstract Product class
abstract class Product implements IProduct {
  constructor(
    public id: number,
    public name: string,
    public price: number,
    protected category: string
  ) {}

  abstract getDiscountedPrice(discountPercent: number): number;

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

// Concrete product classes
class PhysicalProduct extends Product {
  constructor(
    id: number,
    name: string,
    price: number,
    category: string,
    private weightInKg: number,
    private dimensions: {length: number, width: number, height: number}
  ) {
    super(id, name, price, category);
  }

  getDiscountedPrice(discountPercent: number): number {
    return this.price * (1 - discountPercent / 100);
  }

  getShippingCost(): number {
    return this.weightInKg * 2.5;  // $2.50 per kg
  }
}

class DigitalProduct extends Product {
  constructor(
    id: number,
    name: string,
    price: number,
    category: string,
    private fileSizeInMb: number,
    private downloadLink: string
  ) {
    super(id, name, price, category);
  }

  getDiscountedPrice(discountPercent: number): number {
    // Digital products have higher discount
    return this.price * (1 - (discountPercent + 5) / 100);
  }

  getDownloadInfo(): string {
    return `Download from: ${this.downloadLink} (${this.fileSizeInMb} MB)`;
  }
}

// Cart class to manage products
class ShoppingCart {
  private items: IProduct[] = [];

  addItem(product: IProduct): void {
    this.items.push(product);
  }

  removeItem(productId: number): void {
    this.items = this.items.filter(item => item.id !== productId);
  }

  getTotalPrice(): number {
    return this.items.reduce((total, item) => total + item.price, 0);
  }

  getDiscountedTotal(discountPercent: number): number {
    return this.items.reduce(
      (total, item) => total + item.getDiscountedPrice(discountPercent), 
      0
    );
  }
}

// Usage example
const laptop = new PhysicalProduct(
  1, 
  "MacBook Pro", 
  1299.99, 
  "Electronics", 
  2.1, 
  {length: 30.41, width: 21.24, height: 1.56}
);

const ebook = new DigitalProduct(
  2,
  "TypeScript Mastery",
  24.99,
  "Books",
  15.4,
  "https://example.com/downloads/ts-mastery"
);

console.log(laptop.getInfo());  // MacBook Pro - $1299.99 (Electronics)
console.log(`Shipping cost: $${laptop.getShippingCost().toFixed(2)}`);  // Shipping cost: $5.25

console.log(ebook.getInfo());  // TypeScript Mastery - $24.99 (Books)
console.log(ebook.getDownloadInfo());  // Download from: https://example.com/downloads/ts-mastery (15.4 MB)

// Using shopping cart
const cart = new ShoppingCart();
cart.addItem(laptop);
cart.addItem(ebook);

console.log(`Total price: $${cart.getTotalPrice().toFixed(2)}`);  // Total price: $1324.98
console.log(`Discounted total (10%): $${cart.getDiscountedTotal(10).toFixed(2)}`);  // Discounted total (10%): $1177.74

This example demonstrates how TypeScript classes can be used to create a complex system with inheritance, abstract classes, interfaces, and various access modifiers.

Real-World Class Example in TypeScript

Using Getters and Setters in TypeScript Classes

TypeScript supports getters and setters for controlled access to class properties:

class Temperature {
  private _celsius: number;

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

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

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

  // Getter for Fahrenheit
  get fahrenheit(): number {
    return this._celsius * 9/5 + 32;
  }

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

const temp = new Temperature(25);
console.log(`${temp.celsius}°C`);  // 25°C
console.log(`${temp.fahrenheit}°F`);  // 77°F

temp.fahrenheit = 86;
console.log(`${temp.celsius}°C`);  // 30°C

// This would throw an error
// temp.celsius = -300;

In this weather-related example, I’ve implemented getters and setters to control how temperature values are accessed and modified. The setter for celsius ensures that temperatures can’t go below absolute zero, adding validation that would be absent with direct property access.

Use Getters and Setters in TypeScript Classes

Inheritance in TypeScript Classes

Inheritance allows a class to extend another class, inheriting its properties and methods:

class Vehicle {
  constructor(
    protected make: string,
    protected model: string,
    protected year: number
  ) {}

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

  startEngine(): string {
    return "Engine started";
  }
}

class ElectricVehicle extends Vehicle {
  constructor(
    make: string,
    model: string,
    year: number,
    private batteryCapacity: number,
    private range: number
  ) {
    super(make, model, year);
  }

  startEngine(): string {
    return "Electric motor activated silently";
  }

  getBatteryInfo(): string {
    return `Battery: ${this.batteryCapacity} kWh, Range: ${this.range} miles`;
  }

  chargeBattery(percent: number): void {
    console.log(`Charging battery to ${percent}%`);
  }
}

const teslaModelS = new ElectricVehicle("Tesla", "Model S", 2023, 100, 405);
console.log(teslaModelS.getInfo());  // 2023 Tesla Model S
console.log(teslaModelS.startEngine());  // Electric motor activated silently
console.log(teslaModelS.getBatteryInfo());  // Battery: 100 kWh, Range: 405 miles
teslaModelS.chargeBattery(90);  // Charging battery to 90%

In this example, I’ve created a base Vehicle class and extended it with an ElectricVehicle class that adds specific properties and methods relevant to electric vehicles. The ElectricVehicle class also overrides the startEngine method with its own implementation.

Inheritance in TypeScript Classes

Readonly Properties in TypeScript

TypeScript allows you to mark properties as readonly, meaning they can only be set during initialization:

class User {
  readonly id: number;
  name: string;
  readonly createdAt: Date;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
    this.createdAt = new Date();
  }

  updateName(newName: string): void {
    this.name = newName;
    // This would cause an error:
    // this.id = 100;
  }
}

const user = new User(42, "Jane Doe");
console.log(`User ${user.id}: ${user.name}, created on ${user.createdAt.toLocaleDateString()}`);

user.updateName("Jane Smith");
console.log(`Updated name: ${user.name}`);

// This would cause an error:
// user.id = 100;

In this example, the id and createdAt properties are readonly, meaning they can’t be changed after the object is created.

Readonly Properties in TypeScript

Singleton Pattern Using Static Members in TypeScript

The Singleton pattern ensures that a class has only one instance throughout the application:

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private connectionString: string;

  private constructor(connectionString: string) {
    this.connectionString = connectionString;
    console.log(`Connecting to database: ${connectionString}`);
  }

  public static getInstance(connectionString: string): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(connectionString);
    }
    return DatabaseConnection.instance;
  }

  query(sql: string): void {
    console.log(`Executing query on ${this.connectionString}: ${sql}`);
  }
}

// Using the singleton
const db1 = DatabaseConnection.getInstance("mysql://localhost:3306/myapp");
db1.query("SELECT * FROM users");

// This won't create a new connection, it returns the existing instance
const db2 = DatabaseConnection.getInstance("mysql://localhost:3306/myapp");
db2.query("SELECT * FROM products");

// Both are the same instance
console.log(db1 === db2);  // true

In this example, I’ve implemented a singleton pattern for a database connection. No matter how many times you call getInstance(), you’ll always get back the same instance, which is useful for resources that should be shared across your application.

Singleton Pattern Using Static Members in TypeScript

Class Expressions in TypeScript

Similar to function expressions, TypeScript also supports class expressions:

const Point = class {
  constructor(public x: number, public y: number) {}

  getDistance(point: InstanceType<typeof Point>): number {
    const dx = this.x - point.x;
    const dy = this.y - point.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
};

const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
console.log(`Distance: ${p1.getDistance(p2)}`);  // Distance: 5

Class expressions can be useful when you need to create a class dynamically or within a specific scope.

Class Expressions in TypeScript

Conclusion

TypeScript classes are a powerful tool in your development arsenal. They help you structure your code in a clean and organized manner, providing type safety and enabling object-oriented design patterns.

Whether you’re building a simple utility or a complex enterprise application, understanding how to use TypeScript classes effectively will make your code more robust and maintainable. The examples in this article demonstrate how classes can be applied to real-world scenarios that you might encounter in your development career.

You may like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.