Classes are the foundation of object-oriented programming in TypeScript. They let you create blueprints for objects with typed properties, methods, and constructors. TypeScript enhances JavaScript classes with type safety, access modifiers, and powerful OOP features. Let's master classes and constructors!
Basic Class Syntax
A class is a blueprint for creating objects. It defines properties (data) and methods (behavior) that objects will have.
// Basic class definition
class User {
// Properties with types
name: string;
email: string;
age: number;
// Constructor - called when creating new instances
constructor(name: string, email: string, age: number) {
this.name = name;
this.email = email;
this.age = age;
}
// Method
displayInfo(): void {
console.log(`Name: ${this.name}`);
console.log(`Email: ${this.email}`);
console.log(`Age: ${this.age}`);
}
// Method with return value
getGreeting(): string {
return `Hello, my name is ${this.name}`;
}
}
// Create instances using 'new' keyword
const user1 = new User("Yusuf Ibrahim", "yusuf@example.com", 25);
const user2 = new User("Amina Hassan", "amina@example.com", 30);
// Access properties
console.log(user1.name); // "Yusuf Ibrahim"
console.log(user1.email); // "yusuf@example.com"
// Call methods
user1.displayInfo();
// Name: Yusuf Ibrahim
// Email: yusuf@example.com
// Age: 25
console.log(user2.getGreeting()); // "Hello, my name is Amina Hassan"
// Modify properties
user1.age = 26;
console.log(user1.age); // 26💡 The 'this' Keyword
Inside a class, this refers to the current instance. Use this.propertyName to access properties and this.methodName() to call methods.
Constructors
The constructor is a special method called when creating a new instance. It initializes the object's properties.
Basic Constructor
class Product {
id: number;
name: string;
price: number;
constructor(id: number, name: string, price: number) {
this.id = id;
this.name = name;
this.price = price;
console.log(`Product created: ${name}`);
}
getFormattedPrice(): string {
return `₦${this.price.toLocaleString()}`;
}
}
const laptop = new Product(1, "Laptop", 450000);
// Logs: "Product created: Laptop"
console.log(laptop.getFormattedPrice()); // "₦450,000"
const mouse = new Product(2, "Mouse", 5000);
// Logs: "Product created: Mouse"Constructor with Default Values
class Account {
username: string;
email: string;
role: string;
active: boolean;
// Constructor with default parameters
constructor(
username: string,
email: string,
role: string = "user", // Default value
active: boolean = true // Default value
) {
this.username = username;
this.email = email;
this.role = role;
this.active = active;
}
displayStatus(): void {
const status = this.active ? "Active" : "Inactive";
console.log(`${this.username} (${this.role}): ${status}`);
}
}
// Using defaults
const user1 = new Account("yusuf", "yusuf@example.com");
user1.displayStatus(); // "yusuf (user): Active"
// Overriding defaults
const admin = new Account("amina", "amina@example.com", "admin", true);
admin.displayStatus(); // "amina (admin): Active"
const inactive = new Account("chidi", "chidi@example.com", "editor", false);
inactive.displayStatus(); // "chidi (editor): Inactive"Constructor Validation
class BankAccount {
accountNumber: string;
balance: number;
constructor(accountNumber: string, initialBalance: number) {
// Validate account number
if (accountNumber.length !== 10) {
throw new Error("Account number must be 10 digits");
}
// Validate initial balance
if (initialBalance < 0) {
throw new Error("Initial balance cannot be negative");
}
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
this.balance += amount;
console.log(`Deposited ₦${amount}. New balance: ₦${this.balance}`);
}
getBalance(): number {
return this.balance;
}
}
// Valid account
const account1 = new BankAccount("0123456789", 10000);
account1.deposit(5000); // "Deposited ₦5000. New balance: ₦15000"
// Invalid account number
try {
const account2 = new BankAccount("123", 5000);
} catch (error) {
console.error((error as Error).message); // "Account number must be 10 digits"
}
// Invalid initial balance
try {
const account3 = new BankAccount("0123456789", -100);
} catch (error) {
console.error((error as Error).message); // "Initial balance cannot be negative"
}Parameter Properties
TypeScript provides a shorthand for declaring and initializing properties directly in the constructor parameters.
// Traditional approach
class UserTraditional {
name: string;
email: string;
age: number;
constructor(name: string, email: string, age: number) {
this.name = name;
this.email = email;
this.age = age;
}
}
// Parameter properties shorthand
class UserShorthand {
// Adding access modifier creates property automatically
constructor(
public name: string,
public email: string,
public age: number
) {
// No need to assign - it's done automatically!
}
}
const user1 = new UserShorthand("Yusuf", "yusuf@example.com", 25);
console.log(user1.name); // "Yusuf"
console.log(user1.email); // "yusuf@example.com"
console.log(user1.age); // 25
// Can mix parameter properties with regular ones
class Product {
// Parameter properties
constructor(
public id: number,
public name: string,
public price: number
) {
// Can add initialization logic here
console.log(`Product ${this.id} created`);
}
// Regular methods work normally
getFormattedPrice(): string {
return `₦${this.price.toLocaleString()}`;
}
}
const laptop = new Product(1, "Laptop", 450000);
// Logs: "Product 1 created"
console.log(laptop.getFormattedPrice()); // "₦450,000"
// Can combine with default values
class Settings {
constructor(
public theme: string = "light",
public language: string = "en",
public notifications: boolean = true
) {}
}
const defaultSettings = new Settings();
console.log(defaultSettings.theme); // "light"
const customSettings = new Settings("dark", "fr", false);
console.log(customSettings.theme); // "dark"Class Properties
Property Initialization
class User {
// Initialize at declaration
role: string = "user";
active: boolean = true;
loginCount: number = 0;
// Uninitialized (must be set in constructor or marked with !)
name: string;
email: string;
// Readonly property (can only be set in constructor)
readonly id: number;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
this.id = Date.now(); // Set once in constructor
}
incrementLoginCount(): void {
this.loginCount++;
console.log(`Login count: ${this.loginCount}`);
}
}
const user = new User("Yusuf", "yusuf@example.com");
console.log(user.role); // "user" (default)
console.log(user.active); // true (default)
console.log(user.loginCount); // 0 (default)
console.log(user.id); // Timestamp
user.incrementLoginCount(); // "Login count: 1"
user.role = "admin"; // ✅ Can modify
// user.id = 123; // ❌ Error: Cannot assign to 'id' because it is readonly
// Optional properties
class Profile {
name: string;
bio?: string; // Optional property
website?: string; // Optional property
constructor(name: string) {
this.name = name;
}
displayBio(): void {
if (this.bio) {
console.log(this.bio);
} else {
console.log("No bio available");
}
}
}
const profile = new Profile("Yusuf Ibrahim");
profile.displayBio(); // "No bio available"
profile.bio = "Full-stack developer";
profile.displayBio(); // "Full-stack developer"Computed Properties
class Rectangle {
constructor(
public width: number,
public height: number
) {}
// Computed property using getter
get area(): number {
return this.width * this.height;
}
get perimeter(): number {
return 2 * (this.width + this.height);
}
// Setter for validation
set width(value: number) {
if (value <= 0) {
throw new Error("Width must be positive");
}
this.width = value;
}
}
const rect = new Rectangle(10, 20);
console.log(rect.area); // 200 (computed)
console.log(rect.perimeter); // 60 (computed)
// Access like properties (no parentheses)
rect.width = 15;
console.log(rect.area); // 300 (automatically recalculated)
class Person {
constructor(
public firstName: string,
public lastName: string
) {}
// Getter - computed from other properties
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
// Setter - updates multiple properties
set fullName(value: string) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[parts.length - 1];
}
}
const person = new Person("Yusuf", "Ibrahim");
console.log(person.fullName); // "Yusuf Ibrahim"
person.fullName = "Amina Hassan";
console.log(person.firstName); // "Amina"
console.log(person.lastName); // "Hassan"Class Methods
class Calculator {
// Instance property
result: number = 0;
// Instance methods
add(value: number): this {
this.result += value;
return this; // Enable method chaining
}
subtract(value: number): this {
this.result -= value;
return this;
}
multiply(value: number): this {
this.result *= value;
return this;
}
divide(value: number): this {
if (value === 0) {
throw new Error("Cannot divide by zero");
}
this.result /= value;
return this;
}
getResult(): number {
return this.result;
}
reset(): void {
this.result = 0;
}
}
const calc = new Calculator();
// Method chaining
const result = calc
.add(10)
.multiply(5)
.subtract(20)
.divide(3)
.getResult();
console.log(result); // 10
calc.reset();
console.log(calc.getResult()); // 0
// Methods with different signatures
class StringUtils {
static capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
static reverse(str: string): string {
return str.split("").reverse().join("");
}
static truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) {
return str;
}
return str.slice(0, maxLength - 3) + "...";
}
}
console.log(StringUtils.capitalize("hello")); // "Hello"
console.log(StringUtils.reverse("typescript")); // "tpircsepyt"
console.log(StringUtils.truncate("Long text here", 8)); // "Long ..."
// Instance vs Static methods
class Counter {
private count: number = 0;
// Instance method - works with instance data
increment(): void {
this.count++;
}
getCount(): number {
return this.count;
}
// Static method - doesn't use instance data
static createWithInitialValue(value: number): Counter {
const counter = new Counter();
counter.count = value;
return counter;
}
}
const counter1 = new Counter();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 2
const counter2 = Counter.createWithInitialValue(10);
console.log(counter2.getCount()); // 10Practical Examples
Example 1: Shopping Cart
class CartItem {
constructor(
public productId: number,
public name: string,
public price: number,
public quantity: number = 1
) {}
getSubtotal(): number {
return this.price * this.quantity;
}
getFormattedSubtotal(): string {
return `₦${this.getSubtotal().toLocaleString()}`;
}
}
class ShoppingCart {
private items: CartItem[] = [];
addItem(productId: number, name: string, price: number, quantity: number = 1): void {
// Check if item already exists
const existingItem = this.items.find(item => item.productId === productId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push(new CartItem(productId, name, price, quantity));
}
console.log(`Added ${quantity}x ${name} to cart`);
}
removeItem(productId: number): void {
const index = this.items.findIndex(item => item.productId === productId);
if (index !== -1) {
const item = this.items[index];
console.log(`Removed ${item.name} from cart`);
this.items.splice(index, 1);
}
}
updateQuantity(productId: number, quantity: number): void {
const item = this.items.find(item => item.productId === productId);
if (item) {
item.quantity = quantity;
console.log(`Updated ${item.name} quantity to ${quantity}`);
}
}
getTotal(): number {
return this.items.reduce((total, item) => total + item.getSubtotal(), 0);
}
getFormattedTotal(): string {
return `₦${this.getTotal().toLocaleString()}`;
}
getItemCount(): number {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
displayCart(): void {
console.log("\n=== Shopping Cart ===");
if (this.items.length === 0) {
console.log("Cart is empty");
return;
}
this.items.forEach(item => {
console.log(`${item.name} x${item.quantity} - ${item.getFormattedSubtotal()}`);
});
console.log(`\nTotal Items: ${this.getItemCount()}`);
console.log(`Total: ${this.getFormattedTotal()}`);
console.log("===================\n");
}
clear(): void {
this.items = [];
console.log("Cart cleared");
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem(1, "Laptop", 450000, 1);
cart.addItem(2, "Mouse", 5000, 2);
cart.addItem(3, "Keyboard", 15000, 1);
cart.displayCart();
// === Shopping Cart ===
// Laptop x1 - ₦450,000
// Mouse x2 - ₦10,000
// Keyboard x1 - ₦15,000
//
// Total Items: 4
// Total: ₦475,000
// ===================
cart.updateQuantity(2, 3);
cart.displayCart();
cart.removeItem(3);
cart.displayCart();Example 2: Task Manager
type TaskStatus = "pending" | "in-progress" | "completed";
type TaskPriority = "low" | "medium" | "high";
class Task {
readonly id: number;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
readonly createdAt: Date;
updatedAt: Date;
constructor(
title: string,
description: string,
priority: TaskPriority = "medium"
) {
this.id = Date.now();
this.title = title;
this.description = description;
this.status = "pending";
this.priority = priority;
this.createdAt = new Date();
this.updatedAt = new Date();
}
start(): void {
if (this.status !== "pending") {
console.log("Task already started or completed");
return;
}
this.status = "in-progress";
this.updatedAt = new Date();
console.log(`Started task: ${this.title}`);
}
complete(): void {
this.status = "completed";
this.updatedAt = new Date();
console.log(`Completed task: ${this.title}`);
}
updatePriority(priority: TaskPriority): void {
this.priority = priority;
this.updatedAt = new Date();
console.log(`Updated priority to ${priority}`);
}
displayInfo(): void {
console.log(`\n[${this.status.toUpperCase()}] ${this.title}`);
console.log(`Priority: ${this.priority}`);
console.log(`Description: ${this.description}`);
console.log(`Created: ${this.createdAt.toLocaleString()}`);
}
}
class TaskManager {
private tasks: Task[] = [];
addTask(title: string, description: string, priority?: TaskPriority): Task {
const task = new Task(title, description, priority);
this.tasks.push(task);
console.log(`Added task: ${title}`);
return task;
}
getTaskById(id: number): Task | undefined {
return this.tasks.find(task => task.id === id);
}
getTasksByStatus(status: TaskStatus): Task[] {
return this.tasks.filter(task => task.status === status);
}
getTasksByPriority(priority: TaskPriority): Task[] {
return this.tasks.filter(task => task.priority === priority);
}
displayAllTasks(): void {
console.log(`\n=== All Tasks (${this.tasks.length}) ===`);
if (this.tasks.length === 0) {
console.log("No tasks");
return;
}
this.tasks.forEach(task => task.displayInfo());
}
displaySummary(): void {
const pending = this.getTasksByStatus("pending").length;
const inProgress = this.getTasksByStatus("in-progress").length;
const completed = this.getTasksByStatus("completed").length;
console.log(`\n=== Task Summary ===`);
console.log(`Pending: ${pending}`);
console.log(`In Progress: ${inProgress}`);
console.log(`Completed: ${completed}`);
console.log(`Total: ${this.tasks.length}`);
}
}
// Usage
const manager = new TaskManager();
const task1 = manager.addTask(
"Build TypeScript tutorial",
"Create comprehensive TypeScript classes tutorial",
"high"
);
const task2 = manager.addTask(
"Review code",
"Review pull requests",
"medium"
);
const task3 = manager.addTask(
"Update documentation",
"Update API documentation",
"low"
);
task1.start();
task1.complete();
task2.start();
manager.displaySummary();
// === Task Summary ===
// Pending: 1
// In Progress: 1
// Completed: 1
// Total: 3
manager.displayAllTasks();Test Your Knowledge
Type Safety Check
Which class definition is correct?
// Option A
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
// Option B
class User {
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
// Option C
class User {
name: string;
email: string;
}
const user = new User();
user.name = "Yusuf";
user.email = "yusuf@example.com";
// Option D
class User {
constructor(public name: string, public email: string) {
// Properties automatically created
}
}Common TypeScript Error
// Using properties before declaration
class User {
constructor(name: string, email: string) {
this.name = name; // ❌ Error: Property 'name' does not exist
this.email = email; // ❌ Error: Property 'email' does not exist
}
displayInfo() {
console.log(`${this.name}: ${this.email}`);
}
}❌ Property 'name' does not exist on type 'User'. Property 'email' does not exist on type 'User'.
What's Wrong?
TypeScript requires all properties to be declared in the class body before they can be used, even in the constructor. This ensures type safety.
// Fix 1: Declare properties explicitly
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name; // ✅ Works - property declared
this.email = email; // ✅ Works - property declared
}
displayInfo(): void {
console.log(`${this.name}: ${this.email}`);
}
}
const user1 = new User("Yusuf", "yusuf@example.com");
user1.displayInfo(); // "Yusuf: yusuf@example.com"
// Fix 2: Use parameter properties (shorthand)
class UserShorthand {
constructor(
public name: string,
public email: string
) {
// Properties automatically created and initialized
}
displayInfo(): void {
console.log(`${this.name}: ${this.email}`);
}
}
const user2 = new UserShorthand("Amina", "amina@example.com");
user2.displayInfo(); // "Amina: amina@example.com"Solution: Declare properties in the class body or use parameter properties
Best Practices
- Always declare property types - type safety is key
- Use parameter properties for simple classes - reduces boilerplate
- Initialize properties - in declaration or constructor
- Validate in constructor - catch errors early
- Use readonly for immutable properties - prevent accidental changes
- Return 'this' for method chaining - better developer experience
- Keep classes focused - single responsibility principle
- Use getters/setters for computed or validated properties
// ✅ Good: Clear, typed, validated
class Product {
readonly id: number; // Immutable after creation
constructor(
public name: string,
public price: number,
public category: string
) {
// Validation
if (price < 0) {
throw new Error("Price cannot be negative");
}
if (!name.trim()) {
throw new Error("Name is required");
}
this.id = Date.now();
}
// Method chaining
updatePrice(newPrice: number): this {
if (newPrice < 0) {
throw new Error("Price cannot be negative");
}
this.price = newPrice;
return this;
}
// Computed property
get formattedPrice(): string {
return `₦${this.price.toLocaleString()}`;
}
}
// ✅ Good: Focused responsibility
class Logger {
private logs: string[] = [];
log(message: string): void {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
}
getLogs(): readonly string[] {
return [...this.logs]; // Return copy
}
}
// ❌ Avoid: Uninitialized properties
class Bad {
name: string; // Not initialized!
// Will be undefined unless set
}
// ❌ Avoid: No validation
class AlsoBad {
constructor(public price: number) {
// Should validate price > 0
}
}Key Takeaways
- Classes are blueprints for creating typed objects
- Constructors initialize new instances with the
newkeyword - Properties must be declared with types before use
- Parameter properties (with access modifiers) create properties automatically
- Use
thisto access instance properties and methods readonlyproperties can only be set in the constructor- Getters and setters enable computed and validated properties
- Validate constructor parameters to catch errors early
- Return
thisfrom methods to enable method chaining - Classes provide structure for object-oriented programming
What's Next?
Excellent work! You've mastered classes and constructors in TypeScript. Next, we'll explore Access Modifiers—how to control visibility of properties and methods using public, private, and protected. This gives you fine- grained control over encapsulation and data hiding in your classes!
Get ready to master encapsulation and data protection!