Skip to main content

Chapter 3: Classes & Interfaces

Authored by syscook.dev

What are Classes and Interfaces in TypeScript?​

Classes in TypeScript are blueprints for creating objects with properties and methods, similar to JavaScript classes but with enhanced type safety. Interfaces define contracts that classes must implement, providing a way to ensure objects conform to specific structures.

Key Concepts:​

  • Classes: Object-oriented programming constructs with properties, methods, and inheritance
  • Interfaces: Contracts that define the shape of objects
  • Access Modifiers: Control visibility of class members
  • Inheritance: Extending classes and implementing interfaces
  • Abstract Classes: Base classes that cannot be instantiated directly

Why Use Classes and Interfaces?​

1. Object-Oriented Programming​

Classes provide a structured approach to organizing code with encapsulation, inheritance, and polymorphism.

// Class with encapsulation
class BankAccount {
private balance: number;
private accountNumber: string;

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

public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited $${amount}. New balance: $${this.balance}`);
}
}

public withdraw(amount: number): boolean {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
return true;
}
console.log("Insufficient funds or invalid amount");
return false;
}

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

// Usage
const account = new BankAccount("123456", 1000);
account.deposit(500);
account.withdraw(200);
console.log("Current balance:", account.getBalance());

Output:

Deposited $500. New balance: $1500
Withdrew $200. New balance: $1300
Current balance: 1300

2. Type Safety with Interfaces​

Interfaces ensure objects conform to expected structures, catching errors at compile time.

// Interface definition
interface Vehicle {
brand: string;
model: string;
year: number;
start(): void;
stop(): void;
getInfo(): string;
}

// Class implementing interface
class Car implements Vehicle {
brand: string;
model: string;
year: number;
private isRunning: boolean = false;

constructor(brand: string, model: string, year: number) {
this.brand = brand;
this.model = model;
this.year = year;
}

start(): void {
this.isRunning = true;
console.log(`${this.brand} ${this.model} started`);
}

stop(): void {
this.isRunning = false;
console.log(`${this.brand} ${this.model} stopped`);
}

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

// Usage
const myCar = new Car("Toyota", "Camry", 2023);
myCar.start();
console.log(myCar.getInfo());
myCar.stop();

Output:

Toyota Camry started
2023 Toyota Camry
Toyota Camry stopped

How to Use Classes and Interfaces?​

1. Class Fundamentals​

Basic Class Structure​

class Person {
// Properties
public name: string;
private age: number;
protected email: string;
readonly id: number;

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

// Methods
public greet(): string {
return `Hello, I'm ${this.name}`;
}

private validateAge(): boolean {
return this.age >= 0 && this.age <= 150;
}

protected getEmail(): string {
return this.email;
}

// Getter and Setter
get personAge(): number {
return this.age;
}

set personAge(newAge: number) {
if (newAge >= 0 && newAge <= 150) {
this.age = newAge;
} else {
throw new Error("Invalid age");
}
}
}

// Usage
const person = new Person("Alice", 25, "[email protected]");
console.log(person.greet());
console.log("Age:", person.personAge);
person.personAge = 26;
console.log("New age:", person.personAge);

Output:

Hello, I'm Alice
Age: 25
New age: 26

Static Members​

class MathUtils {
// Static property
static readonly PI = 3.14159;

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

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

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

// Usage - no need to instantiate
console.log("PI:", MathUtils.PI);
console.log("Addition:", MathUtils.add(5, 3));
console.log("Multiplication:", MathUtils.multiply(4, 7));
console.log("Circle area:", MathUtils.circleArea(5));

Output:

PI: 3.14159
Addition: 8
Multiplication: 28
Circle area: 78.53975

2. Inheritance​

Basic Inheritance​

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

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

public makeSound(): void {
console.log("Some generic animal sound");
}

public getInfo(): string {
return `${this.name} is ${this.age} years old`;
}
}

// Derived class
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 makeSound(): void {
console.log("Woof! Woof!");
}

// New method specific to Dog
public fetch(): void {
console.log(`${this.name} is fetching the ball`);
}

// Override getInfo to include breed
public getInfo(): string {
return `${super.getInfo()} and is a ${this.breed}`;
}
}

// Usage
const dog = new Dog("Buddy", 3, "Golden Retriever");
console.log(dog.getInfo());
dog.makeSound();
dog.fetch();

Output:

Buddy is 3 years old and is a Golden Retriever
Woof! Woof!
Buddy is fetching the ball

Multiple Inheritance with Interfaces​

// Multiple interfaces
interface Flyable {
fly(): void;
altitude: number;
}

interface Swimmable {
swim(): void;
depth: number;
}

// Class implementing multiple interfaces
class Duck implements Animal, Flyable, Swimmable {
name: string;
age: number;
altitude: number = 0;
depth: number = 0;

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

makeSound(): void {
console.log("Quack! Quack!");
}

getInfo(): string {
return `${this.name} is ${this.age} years old`;
}

fly(): void {
this.altitude = 100;
console.log(`${this.name} is flying at ${this.altitude} meters`);
}

swim(): void {
this.depth = 5;
console.log(`${this.name} is swimming at ${this.depth} meters depth`);
}
}

// Usage
const duck = new Duck("Donald", 2);
console.log(duck.getInfo());
duck.makeSound();
duck.fly();
duck.swim();

Output:

Donald is 2 years old
Quack! Quack!
Donald is flying at 100 meters
Donald is swimming at 5 meters depth

3. Abstract Classes​

// Abstract base class
abstract class Shape {
protected color: string;

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

// Abstract method - must be implemented by derived classes
abstract calculateArea(): number;
abstract calculatePerimeter(): number;

// Concrete method
public getColor(): string {
return this.color;
}

// Concrete method
public displayInfo(): void {
console.log(`Shape color: ${this.color}`);
console.log(`Area: ${this.calculateArea()}`);
console.log(`Perimeter: ${this.calculatePerimeter()}`);
}
}

// Concrete implementation
class Rectangle extends Shape {
private width: number;
private height: number;

constructor(color: string, width: number, height: number) {
super(color);
this.width = width;
this.height = height;
}

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

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

// Another concrete implementation
class Circle extends Shape {
private radius: number;

constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}

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

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

// Usage
const rectangle = new Rectangle("red", 10, 5);
const circle = new Circle("blue", 7);

rectangle.displayInfo();
console.log("---");
circle.displayInfo();

Output:

Shape color: red
Area: 50
Perimeter: 30
---
Shape color: blue
Area: 153.93804002589985
Perimeter: 43.982297150257104

4. Interface Extensions and Merging​

Interface Extension​

// Base interface
interface Person {
name: string;
age: number;
}

// Extended interface
interface Employee extends Person {
employeeId: number;
department: string;
salary: number;
}

// Further extension
interface Manager extends Employee {
teamSize: number;
manageTeam(): void;
}

// Implementation
class ManagerImpl implements Manager {
name: string;
age: number;
employeeId: number;
department: string;
salary: number;
teamSize: number;

constructor(
name: string,
age: number,
employeeId: number,
department: string,
salary: number,
teamSize: number
) {
this.name = name;
this.age = age;
this.employeeId = employeeId;
this.department = department;
this.salary = salary;
this.teamSize = teamSize;
}

manageTeam(): void {
console.log(`${this.name} is managing a team of ${this.teamSize} people`);
}
}

// Usage
const manager = new ManagerImpl("John", 35, 12345, "Engineering", 100000, 8);
manager.manageTeam();

Output:

John is managing a team of 8 people

Interface Merging​

// Interface merging - same interface name
interface User {
id: number;
name: string;
}

interface User {
email: string;
isActive: boolean;
}

// The interfaces are merged into one
const user: User = {
id: 1,
name: "Alice",
email: "[email protected]",
isActive: true
};

console.log("Merged user interface:", user);

Output:

Merged user interface: { id: 1, name: 'Alice', email: '[email protected]', isActive: true }

5. Access Modifiers​

class AccessExample {
// Public - accessible from anywhere
public publicProperty: string = "public";

// Private - only accessible within the class
private privateProperty: string = "private";

// Protected - accessible within the class and derived classes
protected protectedProperty: string = "protected";

// Readonly - can only be set during initialization
readonly readonlyProperty: string = "readonly";

// Public method
public publicMethod(): string {
return this.privateProperty; // Can access private within class
}

// Private method
private privateMethod(): string {
return "This is private";
}

// Protected method
protected protectedMethod(): string {
return this.privateMethod(); // Can access private within class
}
}

class DerivedAccessExample extends AccessExample {
public testAccess(): void {
console.log("Public:", this.publicProperty);
// console.log("Private:", this.privateProperty); // Error: Property 'privateProperty' is private
console.log("Protected:", this.protectedProperty);
console.log("Readonly:", this.readonlyProperty);

console.log("Public method:", this.publicMethod());
// console.log("Private method:", this.privateMethod()); // Error: Property 'privateMethod' is private
console.log("Protected method:", this.protectedMethod());
}
}

// Usage
const example = new DerivedAccessExample();
example.testAccess();

Output:

Public: public
Protected: protected
Readonly: readonly
Public method: private
Protected method: This is private

Practical Examples​

1. E-commerce Product System​

// Base product interface
interface Product {
id: number;
name: string;
price: number;
description: string;
getDisplayInfo(): string;
}

// Abstract base class
abstract class BaseProduct implements Product {
id: number;
name: string;
price: number;
description: string;

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

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

abstract calculateDiscount(): number;
}

// Physical product class
class PhysicalProduct extends BaseProduct {
private weight: number;
private dimensions: { length: number; width: number; height: number };

constructor(
id: number,
name: string,
price: number,
description: string,
weight: number,
dimensions: { length: number; width: number; height: number }
) {
super(id, name, price, description);
this.weight = weight;
this.dimensions = dimensions;
}

calculateDiscount(): number {
return this.price * 0.1; // 10% discount
}

getShippingInfo(): string {
return `Weight: ${this.weight}kg, Dimensions: ${this.dimensions.length}x${this.dimensions.width}x${this.dimensions.height}cm`;
}
}

// Digital product class
class DigitalProduct extends BaseProduct {
private fileSize: number;
private downloadLink: string;

constructor(
id: number,
name: string,
price: number,
description: string,
fileSize: number,
downloadLink: string
) {
super(id, name, price, description);
this.fileSize = fileSize;
this.downloadLink = downloadLink;
}

calculateDiscount(): number {
return this.price * 0.2; // 20% discount for digital products
}

getDownloadInfo(): string {
return `File size: ${this.fileSize}MB, Download: ${this.downloadLink}`;
}
}

// Usage
const laptop = new PhysicalProduct(
1,
"Gaming Laptop",
1500,
"High-performance gaming laptop",
2.5,
{ length: 35, width: 25, height: 2 }
);

const software = new DigitalProduct(
2,
"Photo Editor Pro",
99,
"Professional photo editing software",
500,
"https://example.com/download"
);

console.log(laptop.getDisplayInfo());
console.log("Discount:", laptop.calculateDiscount());
console.log(laptop.getShippingInfo());

console.log("\n" + software.getDisplayInfo());
console.log("Discount:", software.calculateDiscount());
console.log(software.getDownloadInfo());

Output:

Gaming Laptop - $1500
Discount: 150
Weight: 2.5kg, Dimensions: 35x25x2cm

Photo Editor Pro - $99
Discount: 19.8
File size: 500MB, Download: https://example.com/download

2. User Management System​

// User interface
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
lastLogin?: Date;
}

// User roles interface
interface UserRole {
name: string;
permissions: string[];
}

// Base user class
abstract class BaseUser implements User {
id: number;
username: string;
email: string;
isActive: boolean;
lastLogin?: Date;
protected role: UserRole;

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

abstract canAccess(resource: string): boolean;

login(): void {
this.lastLogin = new Date();
console.log(`${this.username} logged in successfully`);
}

logout(): void {
console.log(`${this.username} logged out`);
}

getRoleInfo(): string {
return `Role: ${this.role.name}, Permissions: ${this.role.permissions.join(", ")}`;
}
}

// Regular user class
class RegularUser extends BaseUser {
constructor(id: number, username: string, email: string) {
super(id, username, email, {
name: "User",
permissions: ["read", "comment"]
});
}

canAccess(resource: string): boolean {
return this.role.permissions.includes("read");
}
}

// Admin user class
class AdminUser extends BaseUser {
constructor(id: number, username: string, email: string) {
super(id, username, email, {
name: "Admin",
permissions: ["read", "write", "delete", "admin"]
});
}

canAccess(resource: string): boolean {
return true; // Admin can access everything
}

deleteUser(userId: number): void {
console.log(`Admin ${this.username} deleted user ${userId}`);
}
}

// Usage
const regularUser = new RegularUser(1, "alice", "[email protected]");
const adminUser = new AdminUser(2, "admin", "[email protected]");

regularUser.login();
console.log(regularUser.getRoleInfo());
console.log("Can access posts:", regularUser.canAccess("posts"));

adminUser.login();
console.log(adminUser.getRoleInfo());
console.log("Can access admin panel:", adminUser.canAccess("admin"));
adminUser.deleteUser(1);

Output:

alice logged in successfully
Role: User, Permissions: read, comment
Can access posts: true
admin logged in successfully
Role: Admin, Permissions: read, write, delete, admin
Can access admin panel: true
Admin admin deleted user 1

Best Practices​

1. Use Interfaces for Contracts​

// Good: Use interfaces for contracts
interface Repository<T> {
findById(id: number): T | null;
save(entity: T): T;
delete(id: number): boolean;
}

// Good: Implement the interface
class UserRepository implements Repository<User> {
findById(id: number): User | null {
// Implementation
return null;
}

save(entity: User): User {
// Implementation
return entity;
}

delete(id: number): boolean {
// Implementation
return true;
}
}

2. Use Abstract Classes for Shared Behavior​

// Good: Use abstract classes for shared behavior
abstract class BaseService {
protected validateInput(input: any): boolean {
return input != null;
}

abstract process(data: any): any;
}

3. Prefer Composition Over Inheritance​

// Good: Use composition
class Engine {
start(): void {
console.log("Engine started");
}
}

class Car {
private engine: Engine;

constructor() {
this.engine = new Engine();
}

start(): void {
this.engine.start();
}
}

4. Use Access Modifiers Appropriately​

// Good: Use appropriate access modifiers
class BankAccount {
private balance: number; // Private - internal state
protected accountNumber: string; // Protected - accessible to subclasses
public owner: string; // Public - external access needed
}

Conclusion​

Classes and interfaces in TypeScript provide powerful tools for object-oriented programming with enhanced type safety. By understanding:

  • What classes and interfaces are and their purposes
  • Why they're essential for structured, maintainable code
  • How to use inheritance, access modifiers, and abstract classes

You can create robust, scalable applications with clear contracts and well-organized code structures. These concepts form the foundation for building complex systems with proper encapsulation and polymorphism.

Next Steps​

  • Practice creating class hierarchies
  • Experiment with interface implementations
  • Explore abstract classes and inheritance patterns
  • Move on to Chapter 4: Generics

This tutorial is part of the TypeScript Mastery series by syscook.dev