TypeScript Classes
Build reusable blueprints for objects with properties, methods, and constructor functions.
TypeScript Classes
Master object-oriented programming in TypeScript with free flashcards and interactive examples. This lesson covers class syntax, constructors, access modifiers, inheritance, abstract classes, and static membersโessential concepts for building scalable TypeScript applications.
Welcome to TypeScript Classes ๐ป
Classes are the backbone of object-oriented programming (OOP) in TypeScript. They provide a blueprint for creating objects with specific properties and behaviors, enabling you to write more organized, maintainable, and reusable code. Whether you're building a web application, mobile app, or backend service, understanding classes is crucial for leveraging TypeScript's full potential.
In this lesson, you'll learn how to define classes, work with constructors, control access to members, implement inheritance, and use advanced features like abstract classes and static members. By the end, you'll be equipped to design robust, type-safe class hierarchies that make your code easier to understand and extend.
Core Concepts: Understanding TypeScript Classes ๐๏ธ
What is a Class?
A class is a template for creating objects. It encapsulates data (properties) and functionality (methods) into a single unit. Think of a class as a cookie cutter: it defines the shape, but you need to use it to create actual cookies (objects).
Basic Class Syntax:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Hello, my name is ${this.name}`);
}
}
const alice = new Person("Alice", 30);
alice.greet(); // Output: Hello, my name is Alice
Key Components:
- Properties (
name,age): Store data for each instance - Constructor: Special method that initializes new instances
- Methods (
greet): Functions that define behaviors - Instances: Objects created from the class using
new
๐ก Tip: Always use PascalCase for class names (e.g., UserAccount, ShoppingCart) to distinguish them from regular variables.
Constructors and Initialization โ๏ธ
The constructor is a special method called when you create a new instance of a class. It's where you initialize properties and set up the object's initial state.
Constructor with Parameter Properties:
TypeScript offers a shorthand syntax that automatically creates and assigns properties:
class Product {
constructor(
public name: string,
public price: number,
private inventory: number
) {}
isInStock(): boolean {
return this.inventory > 0;
}
}
const laptop = new Product("Laptop", 999, 5);
console.log(laptop.name); // "Laptop"
// console.log(laptop.inventory); // Error: Property 'inventory' is private
This shorthand automatically:
- Declares the properties
- Sets their types
- Assigns constructor arguments to properties
- Applies access modifiers
Access Modifiers: Controlling Visibility ๐
TypeScript provides three access modifiers to control who can access class members:
| Modifier | Class | Subclasses | Outside | Use Case |
|---|---|---|---|---|
public | โ | โ | โ | Default - accessible everywhere |
private | โ | โ | โ | Internal implementation details |
protected | โ | โ | โ | Shared with subclasses only |
Example:
class BankAccount {
public accountNumber: string;
private balance: number;
protected ownerName: string;
constructor(accountNumber: string, initialBalance: number, owner: string) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.ownerName = owner;
}
public deposit(amount: number): void {
this.balance += amount;
}
public getBalance(): number {
return this.balance; // OK: accessing private member within class
}
}
class SavingsAccount extends BankAccount {
addInterest(rate: number): void {
// this.balance += this.balance * rate; // Error: balance is private
console.log(this.ownerName); // OK: ownerName is protected
}
}
const account = new BankAccount("123456", 1000, "John");
console.log(account.accountNumber); // OK
// console.log(account.balance); // Error: private
// console.log(account.ownerName); // Error: protected
๐ง Memory Device: Think of access modifiers like house security:
- public: Front door (everyone welcome)
- protected: Family room (family members only)
- private: Personal bedroom (just you)
Inheritance: Building Class Hierarchies ๐ณ
Inheritance allows you to create a new class based on an existing class, inheriting its properties and methods. Use the extends keyword.
class Animal {
constructor(public name: string) {}
move(distance: number): void {
console.log(`${this.name} moved ${distance} meters`);
}
}
class Dog extends Animal {
bark(): void {
console.log("Woof! Woof!");
}
}
class Bird extends Animal {
fly(distance: number): void {
console.log(`${this.name} flew ${distance} meters`);
this.move(distance);
}
}
const dog = new Dog("Buddy");
dog.move(10); // Inherited method
dog.bark(); // Dog-specific method
const bird = new Bird("Tweety");
bird.fly(50); // Bird-specific method
Overriding Methods with super:
You can override parent methods and call the parent implementation using super:
class Employee {
constructor(public name: string, public salary: number) {}
getDetails(): string {
return `${this.name} earns $${this.salary}`;
}
}
class Manager extends Employee {
constructor(name: string, salary: number, public department: string) {
super(name, salary); // Call parent constructor
}
getDetails(): string {
return `${super.getDetails()} and manages ${this.department}`;
}
}
const manager = new Manager("Sarah", 80000, "Engineering");
console.log(manager.getDetails());
// Output: Sarah earns $80000 and manages Engineering
โ ๏ธ Important: Always call super() in the child constructor before accessing this.
Abstract Classes: Defining Contracts ๐
Abstract classes serve as base classes that cannot be instantiated directly. They define a contract that subclasses must follow.
abstract class Shape {
constructor(public color: string) {}
abstract getArea(): number; // Must be implemented by subclasses
abstract getPerimeter(): number;
describe(): void {
console.log(`A ${this.color} shape with area ${this.getArea()}`);
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(color: string, public width: number, public height: number) {
super(color);
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
// const shape = new Shape("red"); // Error: Cannot create instance of abstract class
const circle = new Circle("blue", 5);
circle.describe(); // Output: A blue shape with area 78.54...
When to Use Abstract Classes:
- When you want to provide default implementations for some methods
- When subclasses share common functionality
- When you need to enforce a contract with abstract methods
Static Members: Class-Level Properties and Methods ๐ฏ
Static members belong to the class itself, not to instances. They're useful for utility functions, constants, or tracking shared data.
class MathUtils {
static PI: number = 3.14159;
static instanceCount: number = 0;
constructor() {
MathUtils.instanceCount++;
}
static calculateCircleArea(radius: number): number {
return this.PI * radius ** 2;
}
static getInstanceCount(): number {
return MathUtils.instanceCount;
}
}
// Access static members via class name
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // 78.5398...
const util1 = new MathUtils();
const util2 = new MathUtils();
console.log(MathUtils.getInstanceCount()); // 2
Singleton Pattern with Static:
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {} // Private constructor prevents direct instantiation
static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
connect(): void {
console.log("Connected to database");
}
}
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - same instance
Readonly Properties: Immutable Data ๐
The readonly modifier prevents properties from being changed after initialization:
class User {
readonly id: string;
name: string;
constructor(id: string, name: string) {
this.id = id;
this.name = name;
}
updateName(newName: string): void {
this.name = newName; // OK
// this.id = "new-id"; // Error: Cannot assign to 'id' because it is read-only
}
}
const user = new User("u123", "Alice");
user.name = "Alicia"; // OK
// user.id = "u456"; // Error: read-only
๐ก Tip: Use readonly for properties that should never change after object creation, like IDs, timestamps, or configuration values.
Getters and Setters: Controlled Access ๐๏ธ
Getters and setters provide computed properties and validation:
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) {
throw new Error("Temperature below absolute zero!");
}
this._celsius = value;
}
get fahrenheit(): number {
return (this._celsius * 9/5) + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature();
temp.celsius = 25; // Uses setter
console.log(temp.fahrenheit); // Uses getter: 77
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
Example 1: Building a Library System ๐
Let's create a library management system demonstrating multiple class concepts:
abstract class LibraryItem {
constructor(
public readonly id: string,
public title: string,
protected availableCopies: number
) {}
abstract getItemType(): string;
checkout(): boolean {
if (this.availableCopies > 0) {
this.availableCopies--;
return true;
}
return false;
}
returnItem(): void {
this.availableCopies++;
}
isAvailable(): boolean {
return this.availableCopies > 0;
}
}
class Book extends LibraryItem {
constructor(
id: string,
title: string,
availableCopies: number,
public author: string,
public isbn: string
) {
super(id, title, availableCopies);
}
getItemType(): string {
return "Book";
}
getInfo(): string {
return `"${this.title}" by ${this.author} (ISBN: ${this.isbn})`;
}
}
class DVD extends LibraryItem {
constructor(
id: string,
title: string,
availableCopies: number,
public director: string,
public duration: number
) {
super(id, title, availableCopies);
}
getItemType(): string {
return "DVD";
}
getInfo(): string {
return `"${this.title}" directed by ${this.director} (${this.duration} min)`;
}
}
class Library {
private static nextId: number = 1;
private items: LibraryItem[] = [];
static generateId(): string {
return `LIB${String(this.nextId++).padStart(5, '0')}`;
}
addItem(item: LibraryItem): void {
this.items.push(item);
}
findById(id: string): LibraryItem | undefined {
return this.items.find(item => item.id === id);
}
getAvailableItems(): LibraryItem[] {
return this.items.filter(item => item.isAvailable());
}
}
// Usage
const library = new Library();
const book = new Book(
Library.generateId(),
"TypeScript Handbook",
3,
"Microsoft",
"978-0000000000"
);
const dvd = new DVD(
Library.generateId(),
"The Matrix",
2,
"Wachowskis",
136
);
library.addItem(book);
library.addItem(dvd);
console.log(book.checkout()); // true
console.log(book.getInfo());
console.log(library.getAvailableItems().length);
Key Concepts Demonstrated:
- Abstract base class (
LibraryItem) defining common behavior - Inheritance with
BookandDVDextending base class - Protected properties accessible in subclasses
- Static method for ID generation
- Public, private, and protected access control
- Readonly property (
id) that cannot be modified
Example 2: E-Commerce Shopping Cart ๐
A practical example showing class interaction and encapsulation:
class Product {
constructor(
public readonly sku: string,
public name: string,
public price: number,
private stock: number
) {}
isInStock(quantity: number = 1): boolean {
return this.stock >= quantity;
}
reduceStock(quantity: number): void {
if (!this.isInStock(quantity)) {
throw new Error(`Insufficient stock for ${this.name}`);
}
this.stock -= quantity;
}
get stockLevel(): number {
return this.stock;
}
}
class CartItem {
constructor(
public product: Product,
private _quantity: number
) {
if (_quantity <= 0) {
throw new Error("Quantity must be positive");
}
}
get quantity(): number {
return this._quantity;
}
set quantity(value: number) {
if (value <= 0) {
throw new Error("Quantity must be positive");
}
if (!this.product.isInStock(value)) {
throw new Error("Not enough stock");
}
this._quantity = value;
}
get subtotal(): number {
return this.product.price * this._quantity;
}
}
class ShoppingCart {
private items: CartItem[] = [];
addItem(product: Product, quantity: number = 1): void {
const existingItem = this.items.find(item => item.product.sku === product.sku);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push(new CartItem(product, quantity));
}
}
removeItem(sku: string): void {
this.items = this.items.filter(item => item.product.sku !== sku);
}
get total(): number {
return this.items.reduce((sum, item) => sum + item.subtotal, 0);
}
get itemCount(): number {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
checkout(): void {
for (const item of this.items) {
item.product.reduceStock(item.quantity);
}
this.items = [];
}
}
// Usage
const laptop = new Product("LAP001", "Gaming Laptop", 1299.99, 5);
const mouse = new Product("MOU001", "Wireless Mouse", 29.99, 20);
const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(mouse, 2);
console.log(`Total items: ${cart.itemCount}`);
console.log(`Total price: $${cart.total.toFixed(2)}`);
cart.checkout();
console.log(`Laptop stock after checkout: ${laptop.stockLevel}`);
Design Patterns Used:
- Encapsulation: Private properties with controlled access via methods/getters
- Validation: Setters enforce business rules (positive quantities, stock checks)
- Composition:
CartItemcontainsProduct,ShoppingCartcontainsCartItem[] - Computed Properties:
subtotal,total, anditemCountare calculated on-demand
Example 3: Game Character System ๐ฎ
Demonstrating inheritance hierarchy and polymorphism:
abstract class Character {
protected health: number;
protected maxHealth: number;
constructor(
public name: string,
maxHealth: number,
protected attackPower: number
) {
this.maxHealth = maxHealth;
this.health = maxHealth;
}
abstract attack(target: Character): void;
abstract getCharacterType(): string;
takeDamage(damage: number): void {
this.health = Math.max(0, this.health - damage);
console.log(`${this.name} took ${damage} damage. Health: ${this.health}/${this.maxHealth}`);
}
isAlive(): boolean {
return this.health > 0;
}
heal(amount: number): void {
this.health = Math.min(this.maxHealth, this.health + amount);
console.log(`${this.name} healed ${amount}. Health: ${this.health}/${this.maxHealth}`);
}
get healthPercentage(): number {
return (this.health / this.maxHealth) * 100;
}
}
class Warrior extends Character {
private rage: number = 0;
constructor(name: string) {
super(name, 150, 20);
}
attack(target: Character): void {
const damage = this.attackPower + this.rage;
console.log(`${this.name} attacks ${target.name} for ${damage} damage!`);
target.takeDamage(damage);
this.rage = Math.min(50, this.rage + 5); // Build rage
}
getCharacterType(): string {
return "Warrior";
}
specialAbility(): void {
console.log(`${this.name} uses Whirlwind! Rage: ${this.rage}`);
this.rage = 0;
}
}
class Mage extends Character {
private mana: number = 100;
constructor(name: string) {
super(name, 80, 15);
}
attack(target: Character): void {
if (this.mana >= 20) {
const damage = this.attackPower * 2;
console.log(`${this.name} casts Fireball at ${target.name} for ${damage} damage!`);
target.takeDamage(damage);
this.mana -= 20;
} else {
console.log(`${this.name} is out of mana!`);
}
}
getCharacterType(): string {
return "Mage";
}
restoreMana(amount: number): void {
this.mana = Math.min(100, this.mana + amount);
}
}
class Healer extends Character {
constructor(name: string) {
super(name, 100, 10);
}
attack(target: Character): void {
console.log(`${this.name} attacks ${target.name} for ${this.attackPower} damage!`);
target.takeDamage(this.attackPower);
}
getCharacterType(): string {
return "Healer";
}
healAlly(ally: Character): void {
const healAmount = 30;
console.log(`${this.name} heals ${ally.name} for ${healAmount}!`);
ally.heal(healAmount);
}
}
// Battle simulation
const warrior = new Warrior("Thorin");
const mage = new Mage("Gandalf");
const healer = new Healer("Elara");
warrior.attack(mage);
mage.attack(warrior);
healer.healAlly(warrior);
console.log(`${warrior.name} health: ${warrior.healthPercentage.toFixed(1)}%`);
Object-Oriented Principles:
- Polymorphism: Different implementations of
attack()in each subclass - Abstraction:
Characterprovides interface without implementation details - Inheritance: Shared functionality in base class, specialized in subclasses
- Encapsulation: Protected properties prevent direct external manipulation
Example 4: Account Management with Interfaces ๐ฆ
Combining classes with TypeScript interfaces:
interface Auditable {
getAuditLog(): string[];
logAction(action: string): void;
}
interface Transferable {
transfer(amount: number, to: Account): boolean;
}
abstract class Account implements Auditable {
protected balance: number;
protected auditLog: string[] = [];
constructor(
public readonly accountId: string,
protected accountHolder: string,
initialBalance: number
) {
this.balance = initialBalance;
this.logAction(`Account created with balance: ${initialBalance}`);
}
abstract getAccountType(): string;
deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
this.balance += amount;
this.logAction(`Deposited: ${amount}`);
}
getBalance(): number {
return this.balance;
}
logAction(action: string): void {
const timestamp = new Date().toISOString();
this.auditLog.push(`[${timestamp}] ${action}`);
}
getAuditLog(): string[] {
return [...this.auditLog]; // Return copy
}
}
class CheckingAccount extends Account implements Transferable {
private static TRANSFER_FEE = 2.50;
constructor(accountId: string, accountHolder: string, initialBalance: number) {
super(accountId, accountHolder, initialBalance);
}
getAccountType(): string {
return "Checking";
}
withdraw(amount: number): boolean {
if (amount > this.balance) {
this.logAction(`Failed withdrawal: ${amount} (insufficient funds)`);
return false;
}
this.balance -= amount;
this.logAction(`Withdrew: ${amount}`);
return true;
}
transfer(amount: number, to: Account): boolean {
const totalAmount = amount + CheckingAccount.TRANSFER_FEE;
if (totalAmount > this.balance) {
this.logAction(`Failed transfer: ${amount} to ${to.accountId}`);
return false;
}
this.balance -= totalAmount;
to.deposit(amount);
this.logAction(`Transferred: ${amount} to ${to.accountId} (Fee: ${CheckingAccount.TRANSFER_FEE})`);
return true;
}
}
class SavingsAccount extends Account {
constructor(
accountId: string,
accountHolder: string,
initialBalance: number,
private interestRate: number
) {
super(accountId, accountHolder, initialBalance);
}
getAccountType(): string {
return "Savings";
}
applyInterest(): void {
const interest = this.balance * (this.interestRate / 100);
this.balance += interest;
this.logAction(`Interest applied: ${interest.toFixed(2)}`);
}
}
// Usage
const checking = new CheckingAccount("CHK001", "Alice Smith", 1000);
const savings = new SavingsAccount("SAV001", "Bob Johnson", 5000, 2.5);
checking.deposit(500);
checking.transfer(200, savings);
savings.applyInterest();
console.log(`Checking balance: $${checking.getBalance()}`);
console.log(`Savings balance: $${savings.getBalance()}`);
console.log("Audit log:", checking.getAuditLog());
Key Takeaways from This Example:
- Interfaces define contracts that classes must implement
- Multiple interfaces can be implemented by a single class
- Static properties (
TRANSFER_FEE) are shared across all instances - Type safety ensures only accounts with
Transferablecan calltransfer()
Common Mistakes to Avoid โ ๏ธ
1. Forgetting to Call super() in Child Constructor
โ Wrong:
class Employee extends Person {
constructor(name: string, public employeeId: string) {
// Missing super()
this.name = name; // Error: Must call super first
}
}
โ Correct:
class Employee extends Person {
constructor(name: string, public employeeId: string) {
super(name); // Call parent constructor first
// Now can use 'this'
}
}
2. Accessing Private Members from Outside
โ Wrong:
class User {
private password: string;
constructor(password: string) {
this.password = password;
}
}
const user = new User("secret123");
console.log(user.password); // Error: 'password' is private
โ Correct:
class User {
private password: string;
constructor(password: string) {
this.password = password;
}
verifyPassword(input: string): boolean {
return this.password === input;
}
}
3. Modifying Readonly Properties
โ Wrong:
class Order {
constructor(public readonly orderId: string) {}
updateId(newId: string): void {
this.orderId = newId; // Error: Cannot assign to readonly
}
}
โ Correct:
class Order {
constructor(public readonly orderId: string) {}
// Readonly properties cannot be changed after construction
// Create a new instance if you need different ID
}
4. Not Implementing All Abstract Methods
โ Wrong:
abstract class Vehicle {
abstract startEngine(): void;
abstract stopEngine(): void;
}
class Car extends Vehicle {
startEngine(): void {
console.log("Engine started");
}
// Missing stopEngine() - Error!
}
โ Correct:
class Car extends Vehicle {
startEngine(): void {
console.log("Engine started");
}
stopEngine(): void {
console.log("Engine stopped");
}
}
5. Confusing Static and Instance Members
โ Wrong:
class Counter {
static count: number = 0;
increment(): void {
this.count++; // Error: Static accessed via 'this'
}
}
โ Correct:
class Counter {
static count: number = 0;
increment(): void {
Counter.count++; // Use class name for static
}
}
6. Returning Wrong Types from Getters
โ Wrong:
class Product {
private _price: number = 0;
get price(): number {
return `$${this._price}`; // Error: string not assignable to number
}
}
โ Correct:
class Product {
private _price: number = 0;
get price(): number {
return this._price;
}
get formattedPrice(): string {
return `$${this._price.toFixed(2)}`;
}
}
Key Takeaways ๐ฏ
๐ TypeScript Classes Quick Reference
| Concept | Syntax | Purpose |
|---|---|---|
| Basic Class | class Name { } | Define object blueprint |
| Constructor | constructor(params) { } | Initialize instances |
| Public | public prop | Accessible everywhere (default) |
| Private | private prop | Only within class |
| Protected | protected prop | Class + subclasses |
| Readonly | readonly prop | Cannot change after init |
| Static | static prop | Belongs to class, not instance |
| Inheritance | class B extends A | Inherit from parent class |
| Super | super(args) | Call parent constructor/method |
| Abstract Class | abstract class | Cannot instantiate directly |
| Abstract Method | abstract method() | Must implement in subclass |
| Getter | get prop() { } | Computed property (read) |
| Setter | set prop(v) { } | Controlled assignment (write) |
Essential Rules:
- โ
Always call
super()before usingthisin child constructors - โ Use access modifiers to control visibility and protect data
- โ
Make properties
readonlywhen they shouldn't change - โ Implement all abstract methods in concrete subclasses
- โ
Access static members via class name, not
this - โ Use getters/setters for computed or validated properties
- โ Favor composition over deep inheritance hierarchies
Best Practices:
- ๐ฏ Single Responsibility: Each class should have one clear purpose
- ๐ Encapsulation: Keep implementation details private
- ๐๏ธ Composition over Inheritance: Use "has-a" relationships when possible
- ๐ Meaningful Names: Use descriptive class and method names
- โก Avoid God Classes: Don't make one class do everything
๐ Further Study
TypeScript Official Handbook - Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html - Comprehensive guide to TypeScript class features with examples and best practices
MDN Web Docs - Classes: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes - Understanding JavaScript classes that TypeScript builds upon
Refactoring Guru - Design Patterns: https://refactoring.guru/design-patterns/typescript - Learn common OOP design patterns implemented in TypeScript
๐ Congratulations! You now understand TypeScript classes, from basic syntax to advanced concepts like abstract classes and static members. Practice building class hierarchies for real-world scenarios to reinforce these concepts. Remember: good class design makes your code more maintainable, testable, and scalable!