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