TypeScript Blitz - Day 7: Classes & OOP
Master object-oriented programming in TypeScript - classes, inheritance, abstract classes, generics, and design patterns for real-world applications
Goal
After this lesson you’ll master object-oriented programming in TypeScript, using classes with type safety for real-world applications.
Theory Part (20 min)
1. Classes - Basic Syntax
// Basic class definition
class User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
greet(): string {
return `Hello, I'm ${this.name}`;
}
updateEmail(email: string): void {
this.email = email;
}
}
const user = new User(1, "John", "john@example.com");
console.log(user.greet()); // "Hello, I'm John"
// Type of instance
const user2: User = new User(2, "Jane", "jane@example.com");
// Class as type
function processUser(user: User): void {
console.log(user.name);
}
2. Access Modifiers - public, private, protected
class BankAccount {
public accountNumber: string; // Accessible everywhere
private balance: number; // Only within this class
protected owner: string; // Within this class and subclasses
constructor(accountNumber: string, owner: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = initialBalance;
}
// Public method
public deposit(amount: number): void {
this.balance += amount;
}
// Private method
private calculateInterest(): number {
return this.balance * 0.05;
}
// Protected method
protected validateAmount(amount: number): boolean {
return amount > 0;
}
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("123456", "John", 1000);
console.log(account.accountNumber); // ✅ OK - public
// console.log(account.balance); // ❌ Error - private
// console.log(account.owner); // ❌ Error - protected
account.deposit(500); // ✅ OK - public method
// account.calculateInterest(); // ❌ Error - private method
console.log(account.getBalance()); // ✅ OK - 1500
// Subclass can access protected
class SavingsAccount extends BankAccount {
public getOwner(): string {
return this.owner; // ✅ OK - protected is accessible in subclass
}
public addBonus(amount: number): void {
if (this.validateAmount(amount)) { // ✅ OK - protected method
this.deposit(amount);
}
}
}
Default: If no modifier is specified, members are public by default.
3. Parameter Properties - Shorthand Syntax
// Traditional way - verbose
class User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
}
// Parameter properties - concise
class User2 {
constructor(
public id: number,
public name: string,
public email: string
) {
// Properties are automatically created and assigned
}
}
// Both create the same class structure!
const user = new User2(1, "John", "john@example.com");
console.log(user.id); // 1
console.log(user.name); // "John"
// Works with all access modifiers
class Product {
constructor(
public id: number,
public name: string,
private cost: number,
protected supplier: string
) {}
public getProfit(sellingPrice: number): number {
return sellingPrice - this.cost; // ✅ Can access private cost
}
}
4. Readonly Properties
class User {
readonly id: number; // Cannot be changed after initialization
readonly createdAt: Date;
public name: string; // Can be changed
constructor(id: number, name: string) {
this.id = id;
this.createdAt = new Date(); // Set once in constructor
this.name = name;
}
updateName(name: string): void {
this.name = name; // ✅ OK
// this.id = 999; // ❌ Error - readonly
// this.createdAt = new Date(); // ❌ Error - readonly
}
}
// Readonly with parameter properties
class Product {
constructor(
readonly id: string,
readonly sku: string,
public name: string,
public price: number
) {}
}
const product = new Product("1", "SKU-001", "Laptop", 999);
// product.id = "2"; // ❌ Error - readonly
product.name = "Desktop"; // ✅ OK
5. Getters and Setters
class User {
private _age: number;
constructor(
public name: string,
age: number
) {
this._age = age;
}
// Getter - accessed like a property
get age(): number {
return this._age;
}
// Setter - assigned like a property
set age(value: number) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
this._age = value;
}
// Computed property with getter only (read-only)
get birthYear(): number {
return new Date().getFullYear() - this._age;
}
}
const user = new User("John", 30);
// Use like properties, not methods
console.log(user.age); // 30 (calls getter)
user.age = 31; // (calls setter)
console.log(user.age); // 31
console.log(user.birthYear); // Computed value
// user.birthYear = 1990; // ❌ Error - no setter
// Validation in setter
try {
user.age = -5; // Throws error
} catch (e) {
console.error(e.message); // "Invalid age"
}
6. Static Members
class MathUtils {
// Static property - belongs to class, not instances
static PI: number = 3.14159;
// Static method - can be called without creating instance
static calculateCircleArea(radius: number): number {
return this.PI * radius * radius;
}
static max(a: number, b: number): number {
return a > b ? a : b;
}
}
// Call without creating instance
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // 78.53975
console.log(MathUtils.max(10, 20)); // 20
// Static members are not available on instances
const utils = new MathUtils();
// console.log(utils.PI); // ❌ Error - static member
// Practical example: Factory pattern
class User {
constructor(
public id: number,
public name: string
) {}
// Static factory method
static create(name: string): User {
const id = Math.floor(Math.random() * 10000);
return new User(id, name);
}
// Static validation
static isValidName(name: string): boolean {
return name.length > 0 && name.length < 50;
}
}
const user = User.create("John");
console.log(User.isValidName("Jane")); // true
7. Abstract Classes
// Abstract class - cannot be instantiated directly
abstract class Shape {
constructor(public color: string) {}
// Abstract method - must be implemented by subclasses
abstract area(): number;
abstract perimeter(): number;
// Concrete method - inherited by subclasses
describe(): string {
return `A ${this.color} shape with area ${this.area()}`;
}
}
// const shape = new Shape("red"); // ❌ Error - cannot instantiate abstract class
// Concrete class must implement abstract methods
class Circle extends Shape {
constructor(
color: string,
public radius: number
) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(
color: string,
public width: number,
public height: number
) {
super(color);
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
}
const circle = new Circle("red", 5);
console.log(circle.area()); // 78.53981633974483
console.log(circle.describe()); // "A red shape with area 78.53981633974483"
const rectangle = new Rectangle("blue", 10, 5);
console.log(rectangle.area()); // 50
When to use abstract classes:
- Define common behavior with some concrete implementation
- Force subclasses to implement specific methods
- Share code between related classes
- Create base classes for frameworks/libraries
8. Inheritance - extends
// Base class
class Animal {
constructor(public name: string) {}
move(distance: number): void {
console.log(`${this.name} moved ${distance}m`);
}
makeSound(): void {
console.log("Some generic sound");
}
}
// Derived class
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Must call super() before accessing 'this'
}
// Override method
makeSound(): void {
console.log("Woof! Woof!");
}
// New method specific to Dog
fetch(): void {
console.log(`${this.name} is fetching`);
}
}
class Cat extends Animal {
constructor(name: string) {
super(name);
}
// Override method
makeSound(): void {
console.log("Meow!");
}
// Call parent method with super
move(distance: number): void {
console.log("Sneaking...");
super.move(distance); // Call parent implementation
}
}
const dog = new Dog("Buddy", "Labrador");
dog.makeSound(); // "Woof! Woof!" (overridden)
dog.move(10); // "Buddy moved 10m" (inherited)
dog.fetch(); // "Buddy is fetching" (new method)
const cat = new Cat("Whiskers");
cat.makeSound(); // "Meow!" (overridden)
cat.move(5); // "Sneaking..." then "Whiskers moved 5m"
// Type compatibility
const animals: Animal[] = [dog, cat];
animals.forEach(animal => animal.makeSound());
9. Implements - Interface Implementation
// Interface defines contract
interface Drawable {
draw(): void;
color: string;
}
interface Resizable {
resize(scale: number): void;
}
// Class implements single interface
class Circle implements Drawable {
constructor(
public color: string,
public radius: number
) {}
draw(): void {
console.log(`Drawing ${this.color} circle with radius ${this.radius}`);
}
}
// Class implements multiple interfaces
class Rectangle implements Drawable, Resizable {
constructor(
public color: string,
public width: number,
public height: number
) {}
draw(): void {
console.log(`Drawing ${this.color} rectangle ${this.width}x${this.height}`);
}
resize(scale: number): void {
this.width *= scale;
this.height *= scale;
}
}
const rect = new Rectangle("blue", 10, 5);
rect.draw(); // "Drawing blue rectangle 10x5"
rect.resize(2);
rect.draw(); // "Drawing blue rectangle 20x10"
// Interface as type
function renderShape(shape: Drawable): void {
shape.draw();
}
renderShape(new Circle("red", 5));
renderShape(rect);
Difference between extends and implements:
extends- inherits implementation from parent classimplements- must provide implementation for interface contract
10. Private Fields with # (ES2022)
// TypeScript 'private' - compile-time only
class User1 {
private password: string;
constructor(password: string) {
this.password = password;
}
}
// At runtime, password is still accessible (JavaScript has no private)
const user1 = new User1("secret");
console.log((user1 as any).password); // "secret" - can access at runtime!
// ECMAScript private fields with # - runtime privacy
class User2 {
#password: string;
constructor(password: string) {
this.#password = password;
}
verifyPassword(input: string): boolean {
return this.#password === input;
}
}
const user2 = new User2("secret");
// console.log(user2.#password); // ❌ Syntax error - truly private
console.log((user2 as any).password); // undefined - not accessible
// Private methods with #
class BankAccount {
#balance: number = 0;
deposit(amount: number): void {
if (this.#isValidAmount(amount)) {
this.#balance += amount;
}
}
#isValidAmount(amount: number): boolean {
return amount > 0;
}
getBalance(): number {
return this.#balance;
}
}
When to use which:
privatekeyword - TypeScript compile-time checking, better for most cases#private fields - True runtime privacy, use when security matters
11. this Type in Classes
// 'this' as return type for method chaining (fluent interface)
class Calculator {
private value: number = 0;
add(n: number): this {
this.value += n;
return this;
}
subtract(n: number): this {
this.value -= n;
return this;
}
multiply(n: number): this {
this.value *= n;
return this;
}
getResult(): number {
return this.value;
}
}
const result = new Calculator()
.add(10)
.multiply(2)
.subtract(5)
.getResult();
console.log(result); // 15
// 'this' type preserves subclass type
class ScientificCalculator extends Calculator {
power(n: number): this {
const value = this.getResult();
// Implementation
return this;
}
}
const calc = new ScientificCalculator()
.add(5)
.power(2) // Returns ScientificCalculator, not Calculator
.multiply(2);
// this parameter type
class Box {
content: string = "";
// Ensure method is called with correct 'this'
set(this: Box, value: string): void {
this.content = value;
}
}
const box = new Box();
box.set("hello"); // ✅ OK
const set = box.set;
// set("hello"); // ❌ Error - 'this' context lost
12. Generic Classes
// Generic class for type-safe data structures
class Container<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
filter(predicate: (item: T) => boolean): T[] {
return this.items.filter(predicate);
}
}
// Type-safe for numbers
const numbers = new Container<number>();
numbers.add(1);
numbers.add(2);
// numbers.add("3"); // ❌ Error - expects number
// Type-safe for strings
const strings = new Container<string>();
strings.add("hello");
strings.add("world");
// Generic with constraints
class Repository<T extends { id: string | number }> {
private items: Map<string | number, T> = new Map();
save(item: T): void {
this.items.set(item.id, item);
}
findById(id: string | number): T | undefined {
return this.items.get(id);
}
findAll(): T[] {
return Array.from(this.items.values());
}
}
interface User {
id: number;
name: string;
}
const userRepo = new Repository<User>();
userRepo.save({ id: 1, name: "John" });
const user = userRepo.findById(1);
// Multiple type parameters
class Pair<K, V> {
constructor(
public key: K,
public value: V
) {}
swap(): Pair<V, K> {
return new Pair(this.value, this.key);
}
}
const pair = new Pair<string, number>("age", 30);
const swapped = pair.swap(); // Pair<number, string>
Practice Part (35 min)
Exercise 1: E-commerce Shopping Cart System (15 min)
Build a type-safe shopping cart system with classes.
// TODO: Implement the shopping cart system
// 1. Product class
// - Properties: id (readonly), name, price, category
// - Method: getDiscountedPrice(discount: number): number
class Product {
// TODO: Implement
}
// 2. CartItem class
// - Properties: product (readonly), quantity (private with getter/setter)
// - Validate quantity > 0 in setter
// - Method: getTotalPrice(): number
class CartItem {
// TODO: Implement
}
// 3. ShoppingCart class
// - Private items: CartItem[]
// - Methods:
// - addItem(product: Product, quantity: number): this
// - removeItem(productId: string): this
// - updateQuantity(productId: string, quantity: number): this
// - getTotalPrice(): number
// - getItemCount(): number
// - clear(): this
class ShoppingCart {
// TODO: Implement
}
// 4. DiscountCart extends ShoppingCart
// - Add discount functionality
// - Override getTotalPrice to apply discount
class DiscountCart extends ShoppingCart {
// TODO: Implement with discount rate
}
// Tests - uncomment after implementation
// const laptop = new Product("1", "Laptop", 999, "Electronics");
// const mouse = new Product("2", "Mouse", 25, "Electronics");
// const cart = new ShoppingCart();
// cart
// .addItem(laptop, 1)
// .addItem(mouse, 2);
// console.log(cart.getTotalPrice()); // 1049
// console.log(cart.getItemCount()); // 2
// cart.updateQuantity("2", 3);
// console.log(cart.getTotalPrice()); // 1074
// const discountCart = new DiscountCart(0.1); // 10% discount
// discountCart.addItem(laptop, 1);
// console.log(discountCart.getTotalPrice()); // 899.1
Solution:
Click to see solution
class Product {
constructor(
readonly id: string,
public name: string,
public price: number,
public category: string
) {}
getDiscountedPrice(discount: number): number {
return this.price * (1 - discount);
}
}
class CartItem {
private _quantity: number;
constructor(
readonly product: Product,
quantity: number
) {
this._quantity = quantity;
}
get quantity(): number {
return this._quantity;
}
set quantity(value: number) {
if (value <= 0) {
throw new Error("Quantity must be greater than 0");
}
this._quantity = value;
}
getTotalPrice(): number {
return this.product.price * this._quantity;
}
}
class ShoppingCart {
private items: CartItem[] = [];
addItem(product: Product, quantity: number): this {
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push(new CartItem(product, quantity));
}
return this;
}
removeItem(productId: string): this {
this.items = this.items.filter(item => item.product.id !== productId);
return this;
}
updateQuantity(productId: string, quantity: number): this {
const item = this.items.find(item => item.product.id === productId);
if (item) {
item.quantity = quantity;
}
return this;
}
getTotalPrice(): number {
return this.items.reduce((total, item) => total + item.getTotalPrice(), 0);
}
getItemCount(): number {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
clear(): this {
this.items = [];
return this;
}
protected getItems(): CartItem[] {
return this.items;
}
}
class DiscountCart extends ShoppingCart {
constructor(private discountRate: number) {
super();
}
getTotalPrice(): number {
const originalTotal = super.getTotalPrice();
return originalTotal * (1 - this.discountRate);
}
setDiscount(rate: number): this {
this.discountRate = rate;
return this;
}
}
// Tests
const laptop = new Product("1", "Laptop", 999, "Electronics");
const mouse = new Product("2", "Mouse", 25, "Electronics");
const cart = new ShoppingCart();
cart
.addItem(laptop, 1)
.addItem(mouse, 2);
console.log(cart.getTotalPrice()); // 1049
console.log(cart.getItemCount()); // 3
cart.updateQuantity("2", 3);
console.log(cart.getTotalPrice()); // 1074
const discountCart = new DiscountCart(0.1);
discountCart.addItem(laptop, 1);
console.log(discountCart.getTotalPrice()); // 899.1
Exercise 2: Abstract Shape Hierarchy (12 min)
Implement a shape hierarchy using abstract classes and inheritance.
// TODO: Create abstract shape hierarchy
// 1. Abstract Shape class
// - Properties: color (protected)
// - Abstract methods: area(), perimeter()
// - Concrete method: describe(): string
abstract class Shape {
// TODO: Implement
}
// 2. Circle extends Shape
// - Property: radius
// - Implement area and perimeter
class Circle extends Shape {
// TODO: Implement
}
// 3. Rectangle extends Shape
// - Properties: width, height
// - Implement area and perimeter
class Rectangle extends Shape {
// TODO: Implement
}
// 4. Triangle extends Shape
// - Properties: a, b, c (sides)
// - Implement area (use Heron's formula) and perimeter
class Triangle extends Shape {
// TODO: Implement
}
// 5. ShapeCalculator class
// - Static method: totalArea(shapes: Shape[]): number
// - Static method: largestShape(shapes: Shape[]): Shape
class ShapeCalculator {
// TODO: Implement static methods
}
// Tests - uncomment after implementation
// const shapes: Shape[] = [
// new Circle("red", 5),
// new Rectangle("blue", 10, 5),
// new Triangle("green", 3, 4, 5)
// ];
// shapes.forEach(shape => console.log(shape.describe()));
// console.log("Total area:", ShapeCalculator.totalArea(shapes));
// console.log("Largest:", ShapeCalculator.largestShape(shapes).describe());
Solution:
Click to see solution
abstract class Shape {
constructor(protected color: string) {}
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `A ${this.color} ${this.constructor.name} with area ${this.area().toFixed(2)} and perimeter ${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(
color: string,
public width: number,
public height: number
) {
super(color);
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
}
class Triangle extends Shape {
constructor(
color: string,
public a: number,
public b: number,
public c: number
) {
super(color);
}
area(): number {
// Heron's formula
const s = this.perimeter() / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}
perimeter(): number {
return this.a + this.b + this.c;
}
}
class ShapeCalculator {
static totalArea(shapes: Shape[]): number {
return shapes.reduce((total, shape) => total + shape.area(), 0);
}
static largestShape(shapes: Shape[]): Shape {
return shapes.reduce((largest, current) =>
current.area() > largest.area() ? current : largest
);
}
}
// Tests
const shapes: Shape[] = [
new Circle("red", 5),
new Rectangle("blue", 10, 5),
new Triangle("green", 3, 4, 5)
];
shapes.forEach(shape => console.log(shape.describe()));
// A red Circle with area 78.54 and perimeter 31.42
// A blue Rectangle with area 50.00 and perimeter 30.00
// A green Triangle with area 6.00 and perimeter 12.00
console.log("Total area:", ShapeCalculator.totalArea(shapes).toFixed(2));
// Total area: 134.54
console.log("Largest:", ShapeCalculator.largestShape(shapes).describe());
// Largest: A red Circle with area 78.54 and perimeter 31.42
Exercise 3: Generic Repository Pattern (8 min)
Implement a generic repository for data access.
// TODO: Create generic repository pattern
// 1. Entity interface - all entities must have id
interface Entity {
id: string | number;
}
// 2. Generic Repository class
// - Generic type T extends Entity
// - Private storage: Map<string | number, T>
// - Methods:
// - save(entity: T): void
// - findById(id: string | number): T | undefined
// - findAll(): T[]
// - update(id: string | number, updates: Partial<T>): T | undefined
// - delete(id: string | number): boolean
class Repository<T extends Entity> {
// TODO: Implement
}
// 3. Specific repositories extending Repository
interface User extends Entity {
id: number;
name: string;
email: string;
}
interface Product extends Entity {
id: string;
name: string;
price: number;
inStock: boolean;
}
// UserRepository with additional methods
class UserRepository extends Repository<User> {
findByEmail(email: string): User | undefined {
// TODO: Implement
}
}
// ProductRepository with additional methods
class ProductRepository extends Repository<Product> {
findInStock(): Product[] {
// TODO: Implement
}
updatePrice(id: string, newPrice: number): Product | undefined {
// TODO: Implement
}
}
// Tests - uncomment after implementation
// const userRepo = new UserRepository();
// userRepo.save({ id: 1, name: "John", email: "john@example.com" });
// userRepo.save({ id: 2, name: "Jane", email: "jane@example.com" });
// console.log(userRepo.findById(1));
// console.log(userRepo.findByEmail("jane@example.com"));
// const productRepo = new ProductRepository();
// productRepo.save({ id: "P1", name: "Laptop", price: 999, inStock: true });
// productRepo.save({ id: "P2", name: "Mouse", price: 25, inStock: false });
// console.log(productRepo.findInStock());
// productRepo.updatePrice("P1", 899);
Solution:
Click to see solution
interface Entity {
id: string | number;
}
class Repository<T extends Entity> {
protected storage: Map<string | number, T> = new Map();
save(entity: T): void {
this.storage.set(entity.id, entity);
}
findById(id: string | number): T | undefined {
return this.storage.get(id);
}
findAll(): T[] {
return Array.from(this.storage.values());
}
update(id: string | number, updates: Partial<T>): T | undefined {
const entity = this.storage.get(id);
if (entity) {
const updated = { ...entity, ...updates };
this.storage.set(id, updated);
return updated;
}
return undefined;
}
delete(id: string | number): boolean {
return this.storage.delete(id);
}
}
interface User extends Entity {
id: number;
name: string;
email: string;
}
interface Product extends Entity {
id: string;
name: string;
price: number;
inStock: boolean;
}
class UserRepository extends Repository<User> {
findByEmail(email: string): User | undefined {
return this.findAll().find(user => user.email === email);
}
}
class ProductRepository extends Repository<Product> {
findInStock(): Product[] {
return this.findAll().filter(product => product.inStock);
}
updatePrice(id: string, newPrice: number): Product | undefined {
return this.update(id, { price: newPrice });
}
}
// Tests
const userRepo = new UserRepository();
userRepo.save({ id: 1, name: "John", email: "john@example.com" });
userRepo.save({ id: 2, name: "Jane", email: "jane@example.com" });
console.log(userRepo.findById(1));
// { id: 1, name: "John", email: "john@example.com" }
console.log(userRepo.findByEmail("jane@example.com"));
// { id: 2, name: "Jane", email: "jane@example.com" }
const productRepo = new ProductRepository();
productRepo.save({ id: "P1", name: "Laptop", price: 999, inStock: true });
productRepo.save({ id: "P2", name: "Mouse", price: 25, inStock: false });
console.log(productRepo.findInStock());
// [{ id: "P1", name: "Laptop", price: 999, inStock: true }]
productRepo.updatePrice("P1", 899);
console.log(productRepo.findById("P1"));
// { id: "P1", name: "Laptop", price: 899, inStock: true }
Quick Review - Key Concepts
1. What’s the difference between public, private, and protected?
Answer
Access modifiers control visibility of class members:
public - accessible everywhere (default)
class User {
public name: string; // Accessible anywhere
constructor(name: string) {
this.name = name;
}
}
const user = new User("John");
console.log(user.name); // ✅ OK
private - accessible only within the class
class BankAccount {
private balance: number = 0;
deposit(amount: number): void {
this.balance += amount; // ✅ OK - within class
}
getBalance(): number {
return this.balance; // ✅ OK - within class
}
}
const account = new BankAccount();
// account.balance; // ❌ Error - private
account.deposit(100); // ✅ OK
protected - accessible within the class and subclasses
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark(): void {
console.log(`${this.name} says woof`); // ✅ OK - in subclass
}
}
const dog = new Dog("Buddy");
// dog.name; // ❌ Error - protected
dog.bark(); // ✅ OK
When to use:
- public - API methods, properties users should access
- private - internal implementation details, sensitive data
- protected - shared between parent and child classes
Note: TypeScript private is compile-time only. Use # for runtime privacy.
2. What are abstract classes and when to use them?
Answer
Abstract classes cannot be instantiated and may contain abstract methods that subclasses must implement.
Syntax:
abstract class Shape {
constructor(public color: string) {}
// Abstract method - no implementation
abstract area(): number;
// Concrete method - has implementation
describe(): string {
return `A ${this.color} shape`;
}
}
// Cannot instantiate
// const shape = new Shape("red"); // ❌ Error
// Must implement abstract methods
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
When to use abstract classes:
- Define common behavior with some implementation:
abstract class HttpClient {
abstract request(url: string): Promise<Response>;
// Shared implementation
async get(url: string): Promise<Response> {
return this.request(url);
}
async post(url: string, body: any): Promise<Response> {
return this.request(url);
}
}
- Force subclasses to implement specific methods:
abstract class DataProcessor {
abstract validate(data: unknown): boolean;
abstract transform(data: unknown): any;
process(data: unknown): any {
if (this.validate(data)) {
return this.transform(data);
}
throw new Error("Invalid data");
}
}
- Template method pattern:
abstract class Game {
// Template method
play(): void {
this.initialize();
this.startPlay();
this.endPlay();
}
abstract initialize(): void;
abstract startPlay(): void;
abstract endPlay(): void;
}
Abstract class vs Interface:
- Abstract class: Can have implementation, single inheritance
- Interface: No implementation, multiple inheritance (implements)
3. How do getters and setters work?
Answer
Getters and setters allow controlled access to properties, accessed like properties but executed as methods.
Basic syntax:
class User {
private _age: number;
constructor(age: number) {
this._age = age;
}
// Getter
get age(): number {
return this._age;
}
// Setter
set age(value: number) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
this._age = value;
}
}
const user = new User(30);
console.log(user.age); // Calls getter, returns 30
user.age = 31; // Calls setter
Common use cases:
- Validation:
class Product {
private _price: number;
set price(value: number) {
if (value < 0) {
throw new Error("Price cannot be negative");
}
this._price = value;
}
get price(): number {
return this._price;
}
}
- Computed properties:
class Rectangle {
constructor(
public width: number,
public height: number
) {}
get area(): number {
return this.width * this.height;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area); // 50 (computed)
- Lazy initialization:
class Database {
private _connection?: Connection;
get connection(): Connection {
if (!this._connection) {
this._connection = createConnection();
}
return this._connection;
}
}
- Encapsulation:
class BankAccount {
private _balance: number = 0;
get balance(): number {
return this._balance; // Read-only from outside
}
deposit(amount: number): void {
this._balance += amount;
}
}
Benefits:
- Validation on assignment
- Computed values
- Encapsulation (hide internal state)
- API compatibility (can add logic later)
4. What’s the difference between extends and implements?
Answer
extends - inherits implementation from a class
implements - provides implementation for an interface
extends (inheritance):
class Animal {
constructor(public name: string) {}
move(): void {
console.log(`${this.name} is moving`);
}
}
class Dog extends Animal {
bark(): void {
console.log("Woof!");
}
}
const dog = new Dog("Buddy");
dog.move(); // ✅ Inherited from Animal
dog.bark(); // ✅ Own method
implements (interface contract):
interface Drawable {
draw(): void;
color: string;
}
class Circle implements Drawable {
constructor(public color: string) {}
// Must implement all interface members
draw(): void {
console.log(`Drawing ${this.color} circle`);
}
}
Key differences:
| Feature | extends | implements |
|---|---|---|
| Inherits code | Yes | No |
| Multiple | No (single) | Yes (multiple interfaces) |
| Must implement | Only abstract | Everything |
| Access to parent | super keyword | No |
Can combine both:
abstract class Shape {
constructor(public color: string) {}
abstract area(): number;
}
interface Drawable {
draw(): void;
}
class Circle extends Shape implements Drawable {
constructor(color: string, public radius: number) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
draw(): void {
console.log(`Drawing circle`);
}
}
When to use:
- extends - code reuse, is-a relationship
- implements - contract enforcement, can-do relationship
5. How do generic classes work?
Answer
Generic classes use type parameters to create reusable, type-safe components.
Basic syntax:
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
const numberBox = new Box<number>(123);
const stringBox = new Box<string>("hello");
numberBox.setValue(456); // ✅ OK
// numberBox.setValue("text"); // ❌ Error
With constraints:
interface HasId {
id: string | number;
}
class Repository<T extends HasId> {
private items: Map<string | number, T> = new Map();
save(item: T): void {
this.items.set(item.id, item); // ✅ item.id guaranteed to exist
}
findById(id: string | number): T | undefined {
return this.items.get(id);
}
}
interface User {
id: number;
name: string;
}
const userRepo = new Repository<User>();
Multiple type parameters:
class Pair<K, V> {
constructor(
public key: K,
public value: V
) {}
swap(): Pair<V, K> {
return new Pair(this.value, this.key);
}
}
const pair = new Pair<string, number>("age", 30);
const swapped = pair.swap(); // Pair<number, string>
Generic methods in classes:
class Utils {
static map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
}
const numbers = [1, 2, 3];
const strings = Utils.map(numbers, n => n.toString()); // string[]
Common patterns:
- Collections:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
- Data access:
class Cache<K, V> {
private storage = new Map<K, V>();
get(key: K): V | undefined {
return this.storage.get(key);
}
set(key: K, value: V): void {
this.storage.set(key, value);
}
}
Benefits:
- Type safety without code duplication
- Reusable across different types
- Compile-time type checking
- Better IDE support
Checklist - What You Should Know After Day 7
- Create classes with properties and methods
- Use access modifiers (public, private, protected)
- Write parameter properties for concise constructors
- Implement readonly properties
- Create getters and setters
- Use static members and methods
- Define and extend abstract classes
- Implement inheritance with extends
- Implement interfaces with implements
- Create generic classes with type parameters
Quick Reference Card
// Basic class
class User {
constructor(
public id: number,
public name: string,
private password: string
) {}
greet(): string {
return `Hello, ${this.name}`;
}
}
// Access modifiers
public name: string; // Accessible everywhere
private password: string; // Only in this class
protected role: string; // This class and subclasses
// Readonly
readonly id: number;
// Getters/Setters
get age(): number { return this._age; }
set age(value: number) { this._age = value; }
// Static
static create(): User { return new User(); }
// Abstract class
abstract class Shape {
abstract area(): number;
describe(): string {
return `Area: ${this.area()}`;
}
}
// Inheritance
class Circle extends Shape {
area(): number {
return Math.PI * this.radius ** 2;
}
}
// Implements
class MyClass implements Interface1, Interface2 {
// Must implement all interface members
}
// Generic class
class Box<T> {
constructor(private value: T) {}
getValue(): T {
return this.value;
}
}
// Private fields (runtime)
class User {
#password: string;
constructor(password: string) {
this.#password = password;
}
}
Tomorrow: Design Patterns in TypeScript - Singleton, Factory, Observer, and more