Master TypeScript interfaces, classes, inheritance, and advanced OOP patterns. Learn to create robust, scalable object-oriented architectures with strong typing and modern design patterns.
Object-oriented programming in TypeScript combines the power of modern JavaScript classes with robust type safety. Interfaces define contracts that ensure your objects have the right shape, while classes provide blueprints for creating instances with methods and properties.
TypeScript's OOP features go beyond basic classes - you'll learn interfaces, abstract classes, access modifiers, and advanced patterns that create maintainable, scalable architectures. These concepts are essential for enterprise applications and modern frameworks like Angular, which heavily rely on TypeScript's OOP capabilities.
In this comprehensive lesson, you'll master both the fundamentals and advanced patterns of object-oriented TypeScript, enabling you to build sophisticated applications with confidence.
Interfaces are the foundation of TypeScript's structural typing system. They define contracts that objects must fulfill, creating predictable and maintainable code.
Basic Interface Definition:
1interface User {
2 id: number;
3 name: string;
4 email: string;
5 age: number;
6}
7
8// Object must match interface exactly
9const user: User = {
10 id: 1,
11 name: "John Doe",
12 email: "[email protected]",
13 age: 30
14};
15
16// Missing properties cause errors
17// const invalidUser: User = { id: 1 }; // Error: missing name, email, ageOptional and Readonly Properties:
1interface Product {
2 readonly id: number; // Cannot be modified after creation
3 name: string;
4 description?: string; // Optional property
5 price: number;
6 categories?: string[]; // Optional array
7 readonly createdAt: Date; // Readonly timestamp
8}
9
10const product: Product = {
11 id: 1,
12 name: "Laptop",
13 price: 999.99,
14 createdAt: new Date()
15};
16
17// product.id = 2; // Error: Cannot assign to 'id' because it is read-only
18product.description = "High-performance laptop"; // OK - optional property can be addedMethod Signatures in Interfaces:
1interface Calculator {
2 add(a: number, b: number): number;
3 subtract(a: number, b: number): number;
4 multiply(a: number, b: number): number;
5 divide(a: number, b: number): number;
6 history: string[];
7
8 // Method with optional parameters
9 calculate?(operation: string, ...operands: number[]): number;
10}
11
12class BasicCalculator implements Calculator {
13 history: string[] = [];
14
15 add(a: number, b: number): number {
16 const result = a + b;
17 this.history.push(`${a} + ${b} = ${result}`);
18 return result;
19 }
20
21 subtract(a: number, b: number): number {
22 const result = a - b;
23 this.history.push(`${a} - ${b} = ${result}`);
24 return result;
25 }
26
27 multiply(a: number, b: number): number {
28 const result = a * b;
29 this.history.push(`${a} * ${b} = ${result}`);
30 return result;
31 }
32
33 divide(a: number, b: number): number {
34 if (b === 0) throw new Error("Division by zero");
35 const result = a / b;
36 this.history.push(`${a} / ${b} = ${result}`);
37 return result;
38 }
39}Index Signatures:
1// Interface with dynamic properties
2interface ApiResponse {
3 status: number;
4 message: string;
5 [key: string]: any; // Any additional properties
6}
7
8interface StringDictionary {
9 [key: string]: string;
10}
11
12interface NumberDictionary {
13 [key: string]: number;
14 // Can also specify known properties
15 length: number;
16 count: number;
17}
18
19const config: StringDictionary = {
20 apiUrl: "https://api.example.com",
21 version: "1.0.0",
22 environment: "production"
23};Extending Interfaces:
1interface BaseEntity {
2 id: number;
3 createdAt: Date;
4 updatedAt: Date;
5}
6
7interface User extends BaseEntity {
8 name: string;
9 email: string;
10 role: "admin" | "user" | "moderator";
11}
12
13interface Post extends BaseEntity {
14 title: string;
15 content: string;
16 authorId: number;
17 tags: string[];
18}
19
20// Multiple inheritance
21interface AdminUser extends User {
22 permissions: string[];
23 lastLogin?: Date;
24}
25
26// Interface can extend multiple interfaces
27interface BlogPost extends Post {
28 publishedAt?: Date;
29 featured: boolean;
30}Function Type Interfaces:
1// Interface for function types
2interface SearchFunction {
3 (source: string, subString: string): boolean;
4}
5
6interface EventListener<T> {
7 (event: T): void;
8}
9
10interface AsyncProcessor<T, U> {
11 (input: T): Promise<U>;
12}
13
14// Usage
15const stringSearch: SearchFunction = (src, sub) => src.includes(sub);
16
17const clickHandler: EventListener<MouseEvent> = (event) => {
18 console.log(`Clicked at: ${event.clientX}, ${event.clientY}`);
19};
20
21const dataProcessor: AsyncProcessor<string, number> = async (data) => {
22 return data.length;
23};TypeScript classes extend JavaScript classes with static typing, access modifiers, and advanced OOP features that enable robust application architecture.
Basic Class Definition:
1class Person {
2 // Property declarations with types
3 public name: string;
4 private age: number;
5 protected email: string;
6 readonly id: number;
7
8 constructor(name: string, age: number, email: string) {
9 this.name = name;
10 this.age = age;
11 this.email = email;
12 this.id = Math.floor(Math.random() * 1000);
13 }
14
15 // Public method
16 public introduce(): string {
17 return `Hi, I'm ${this.name}`;
18 }
19
20 // Private method (only accessible within class)
21 private validateAge(age: number): boolean {
22 return age >= 0 && age <= 150;
23 }
24
25 // Protected method (accessible in subclasses)
26 protected updateEmail(newEmail: string): void {
27 if (newEmail.includes('@')) {
28 this.email = newEmail;
29 }
30 }
31
32 // Getter and setter
33 get displayAge(): string {
34 return `${this.age} years old`;
35 }
36
37 set updateAge(newAge: number) {
38 if (this.validateAge(newAge)) {
39 this.age = newAge;
40 }
41 }
42}Parameter Properties (Shorthand):
1// Concise class definition using parameter properties
2class Product {
3 constructor(
4 public readonly id: number,
5 public name: string,
6 private _price: number,
7 protected category: string
8 ) {}
9
10 get price(): number {
11 return this._price;
12 }
13
14 set price(value: number) {
15 if (value > 0) {
16 this._price = value;
17 }
18 }
19
20 public getCategory(): string {
21 return this.category;
22 }
23}
24
25// Equivalent to the longer form but much more concise
26const product = new Product(1, "Laptop", 999.99, "Electronics");Static Members:
1class MathUtils {
2 static readonly PI = 3.14159;
3 private static instanceCount = 0;
4
5 static add(a: number, b: number): number {
6 return a + b;
7 }
8
9 static multiply(a: number, b: number): number {
10 return a * b;
11 }
12
13 static getInstanceCount(): number {
14 return MathUtils.instanceCount;
15 }
16
17 constructor() {
18 MathUtils.instanceCount++;
19 }
20}
21
22// Usage without instantiation
23console.log(MathUtils.PI);
24console.log(MathUtils.add(5, 3));
25
26const math1 = new MathUtils();
27const math2 = new MathUtils();
28console.log(MathUtils.getInstanceCount()); // 2Abstract Classes:
1abstract class Animal {
2 protected name: string;
3 protected age: number;
4
5 constructor(name: string, age: number) {
6 this.name = name;
7 this.age = age;
8 }
9
10 // Concrete method available to all subclasses
11 public getInfo(): string {
12 return `${this.name} is ${this.age} years old`;
13 }
14
15 // Abstract method must be implemented by subclasses
16 abstract makeSound(): string;
17 abstract move(): string;
18
19 // Abstract getter
20 abstract get species(): string;
21}
22
23class Dog extends Animal {
24 private breed: string;
25
26 constructor(name: string, age: number, breed: string) {
27 super(name, age);
28 this.breed = breed;
29 }
30
31 makeSound(): string {
32 return "Woof! Woof!";
33 }
34
35 move(): string {
36 return "Running on four legs";
37 }
38
39 get species(): string {
40 return `${this.breed} Dog`;
41 }
42
43 // Additional method specific to Dog
44 public fetch(): string {
45 return `${this.name} is fetching the ball!`;
46 }
47}
48
49// Cannot instantiate abstract class
50// const animal = new Animal("Generic", 5); // Error
51
52const dog = new Dog("Buddy", 3, "Golden Retriever");
53console.log(dog.getInfo()); // "Buddy is 3 years old"
54console.log(dog.makeSound()); // "Woof! Woof!"
55console.log(dog.species); // "Golden Retriever Dog"Implementing Interfaces:
1interface Flyable {
2 altitude: number;
3 fly(): void;
4 land(): void;
5}
6
7interface Swimmable {
8 depth: number;
9 swim(): void;
10 surface(): void;
11}
12
13// Class implementing multiple interfaces
14class Duck extends Animal implements Flyable, Swimmable {
15 altitude: number = 0;
16 depth: number = 0;
17
18 constructor(name: string, age: number) {
19 super(name, age);
20 }
21
22 makeSound(): string {
23 return "Quack!";
24 }
25
26 move(): string {
27 return "Waddling";
28 }
29
30 get species(): string {
31 return "Duck";
32 }
33
34 // Implementing Flyable
35 fly(): void {
36 this.altitude = 100;
37 console.log(`${this.name} is flying at ${this.altitude} feet`);
38 }
39
40 land(): void {
41 this.altitude = 0;
42 console.log(`${this.name} has landed`);
43 }
44
45 // Implementing Swimmable
46 swim(): void {
47 this.depth = 5;
48 console.log(`${this.name} is swimming at ${this.depth} feet deep`);
49 }
50
51 surface(): void {
52 this.depth = 0;
53 console.log(`${this.name} has surfaced`);
54 }
55}TypeScript inheritance enables code reuse and polymorphism, allowing objects of different types to be treated uniformly through common interfaces or base classes.
Class Inheritance with Method Override:
1class Vehicle {
2 protected make: string;
3 protected model: string;
4 protected year: number;
5
6 constructor(make: string, model: string, year: number) {
7 this.make = make;
8 this.model = model;
9 this.year = year;
10 }
11
12 public getInfo(): string {
13 return `${this.year} ${this.make} ${this.model}`;
14 }
15
16 public start(): string {
17 return "Vehicle is starting...";
18 }
19
20 public stop(): string {
21 return "Vehicle is stopping...";
22 }
23}
24
25class Car extends Vehicle {
26 private doors: number;
27 private fuelType: "gasoline" | "electric" | "hybrid";
28
29 constructor(make: string, model: string, year: number, doors: number, fuelType: "gasoline" | "electric" | "hybrid") {
30 super(make, model, year);
31 this.doors = doors;
32 this.fuelType = fuelType;
33 }
34
35 // Override parent method
36 public start(): string {
37 return `${this.getInfo()} is starting with ${this.fuelType} engine...`;
38 }
39
40 // Additional method specific to Car
41 public openTrunk(): string {
42 return "Trunk is opened";
43 }
44
45 public getFuelType(): string {
46 return this.fuelType;
47 }
48}
49
50class Motorcycle extends Vehicle {
51 private engineSize: number;
52
53 constructor(make: string, model: string, year: number, engineSize: number) {
54 super(make, model, year);
55 this.engineSize = engineSize;
56 }
57
58 // Override parent method
59 public start(): string {
60 return `${this.getInfo()} (${this.engineSize}cc) is revving up...`;
61 }
62
63 public wheelie(): string {
64 return "Performing a wheelie!";
65 }
66}Polymorphism in Action:
1// Polymorphism allows treating different objects uniformly
2class VehicleManager {
3 private vehicles: Vehicle[] = [];
4
5 addVehicle(vehicle: Vehicle): void {
6 this.vehicles.push(vehicle);
7 }
8
9 startAllVehicles(): string[] {
10 return this.vehicles.map(vehicle => vehicle.start());
11 }
12
13 getVehicleInfo(): string[] {
14 return this.vehicles.map(vehicle => vehicle.getInfo());
15 }
16}
17
18const manager = new VehicleManager();
19manager.addVehicle(new Car("Toyota", "Camry", 2023, 4, "hybrid"));
20manager.addVehicle(new Motorcycle("Honda", "CBR600RR", 2022, 600));
21manager.addVehicle(new Car("Tesla", "Model 3", 2023, 4, "electric"));
22
23// Each vehicle's start() method behaves differently (polymorphism)
24console.log(manager.startAllVehicles());Interface-Based Polymorphism:
1interface Drawable {
2 draw(): string;
3 getArea(): number;
4}
5
6interface Colorable {
7 color: string;
8 setColor(color: string): void;
9}
10
11class Circle implements Drawable, Colorable {
12 color: string = "black";
13
14 constructor(private radius: number) {}
15
16 draw(): string {
17 return `Drawing a ${this.color} circle with radius ${this.radius}`;
18 }
19
20 getArea(): number {
21 return Math.PI * this.radius * this.radius;
22 }
23
24 setColor(color: string): void {
25 this.color = color;
26 }
27}
28
29class Rectangle implements Drawable, Colorable {
30 color: string = "black";
31
32 constructor(private width: number, private height: number) {}
33
34 draw(): string {
35 return `Drawing a ${this.color} rectangle ${this.width}x${this.height}`;
36 }
37
38 getArea(): number {
39 return this.width * this.height;
40 }
41
42 setColor(color: string): void {
43 this.color = color;
44 }
45}
46
47// Polymorphic usage through interfaces
48function drawShapes(shapes: Drawable[]): void {
49 shapes.forEach(shape => {
50 console.log(shape.draw());
51 console.log(`Area: ${shape.getArea()}`);
52 });
53}
54
55const shapes: Drawable[] = [
56 new Circle(5),
57 new Rectangle(10, 8),
58 new Circle(3)
59];
60
61drawShapes(shapes);Generic Classes:
1class Repository<T> {
2 private items: T[] = [];
3 private nextId: number = 1;
4
5 add(item: T & { id?: number }): T & { id: number } {
6 const itemWithId = { ...item, id: this.nextId++ };
7 this.items.push(itemWithId);
8 return itemWithId;
9 }
10
11 findById(id: number): (T & { id: number }) | undefined {
12 return this.items.find(item => (item as any).id === id);
13 }
14
15 findAll(): (T & { id: number })[] {
16 return [...this.items];
17 }
18
19 update(id: number, updates: Partial<T>): boolean {
20 const index = this.items.findIndex(item => (item as any).id === id);
21 if (index !== -1) {
22 this.items[index] = { ...this.items[index], ...updates };
23 return true;
24 }
25 return false;
26 }
27
28 delete(id: number): boolean {
29 const index = this.items.findIndex(item => (item as any).id === id);
30 if (index !== -1) {
31 this.items.splice(index, 1);
32 return true;
33 }
34 return false;
35 }
36
37 count(): number {
38 return this.items.length;
39 }
40}
41
42// Usage with different types
43interface User {
44 name: string;
45 email: string;
46}
47
48interface Product {
49 name: string;
50 price: number;
51 category: string;
52}
53
54const userRepo = new Repository<User>();
55const productRepo = new Repository<Product>();
56
57const user = userRepo.add({ name: "John", email: "[email protected]" });
58const product = productRepo.add({ name: "Laptop", price: 999.99, category: "Electronics" });
59
60console.log(userRepo.findById(user.id));
61console.log(productRepo.findAll());TypeScript's advanced features enable sophisticated design patterns that create maintainable, scalable applications. These patterns leverage TypeScript's type system for better code organization.
Singleton Pattern with TypeScript:
1class DatabaseConnection {
2 private static instance: DatabaseConnection;
3 private connectionString: string;
4 private isConnected: boolean = false;
5
6 private constructor() {
7 this.connectionString = "default-connection";
8 }
9
10 public static getInstance(): DatabaseConnection {
11 if (!DatabaseConnection.instance) {
12 DatabaseConnection.instance = new DatabaseConnection();
13 }
14 return DatabaseConnection.instance;
15 }
16
17 public connect(connectionString?: string): void {
18 if (connectionString) {
19 this.connectionString = connectionString;
20 }
21 this.isConnected = true;
22 console.log(`Connected to: ${this.connectionString}`);
23 }
24
25 public disconnect(): void {
26 this.isConnected = false;
27 console.log("Database disconnected");
28 }
29
30 public query<T>(sql: string): Promise<T[]> {
31 if (!this.isConnected) {
32 throw new Error("Database not connected");
33 }
34 // Simulate database query
35 return Promise.resolve([] as T[]);
36 }
37}
38
39// Usage
40const db1 = DatabaseConnection.getInstance();
41const db2 = DatabaseConnection.getInstance();
42console.log(db1 === db2); // true - same instanceFactory Pattern with Generic Types:
1interface Animal {
2 name: string;
3 makeSound(): string;
4 move(): string;
5}
6
7class Dog implements Animal {
8 constructor(public name: string) {}
9
10 makeSound(): string {
11 return "Woof!";
12 }
13
14 move(): string {
15 return "Running";
16 }
17}
18
19class Cat implements Animal {
20 constructor(public name: string) {}
21
22 makeSound(): string {
23 return "Meow!";
24 }
25
26 move(): string {
27 return "Prowling";
28 }
29}
30
31class Bird implements Animal {
32 constructor(public name: string) {}
33
34 makeSound(): string {
35 return "Tweet!";
36 }
37
38 move(): string {
39 return "Flying";
40 }
41}
42
43type AnimalType = "dog" | "cat" | "bird";
44
45class AnimalFactory {
46 private static animalConstructors: Record<AnimalType, new (name: string) => Animal> = {
47 dog: Dog,
48 cat: Cat,
49 bird: Bird
50 };
51
52 static createAnimal(type: AnimalType, name: string): Animal {
53 const AnimalConstructor = this.animalConstructors[type];
54 if (!AnimalConstructor) {
55 throw new Error(`Unknown animal type: ${type}`);
56 }
57 return new AnimalConstructor(name);
58 }
59
60 static getSupportedTypes(): AnimalType[] {
61 return Object.keys(this.animalConstructors) as AnimalType[];
62 }
63}
64
65// Usage
66const animals = [
67 AnimalFactory.createAnimal("dog", "Buddy"),
68 AnimalFactory.createAnimal("cat", "Whiskers"),
69 AnimalFactory.createAnimal("bird", "Tweety")
70];
71
72animals.forEach(animal => {
73 console.log(`${animal.name} says: ${animal.makeSound()}`);
74});Observer Pattern with TypeScript:
1interface Observer<T> {
2 update(data: T): void;
3}
4
5interface Subject<T> {
6 attach(observer: Observer<T>): void;
7 detach(observer: Observer<T>): void;
8 notify(data: T): void;
9}
10
11class EventEmitter<T> implements Subject<T> {
12 private observers: Observer<T>[] = [];
13
14 attach(observer: Observer<T>): void {
15 if (!this.observers.includes(observer)) {
16 this.observers.push(observer);
17 }
18 }
19
20 detach(observer: Observer<T>): void {
21 const index = this.observers.indexOf(observer);
22 if (index > -1) {
23 this.observers.splice(index, 1);
24 }
25 }
26
27 notify(data: T): void {
28 this.observers.forEach(observer => observer.update(data));
29 }
30}
31
32// News system example
33interface NewsUpdate {
34 headline: string;
35 content: string;
36 category: "sports" | "tech" | "politics" | "entertainment";
37 timestamp: Date;
38}
39
40class NewsSubscriber implements Observer<NewsUpdate> {
41 constructor(private name: string, private interestedCategories: string[]) {}
42
43 update(news: NewsUpdate): void {
44 if (this.interestedCategories.includes(news.category)) {
45 console.log(`${this.name} received: ${news.headline}`);
46 }
47 }
48}
49
50class NewsPublisher extends EventEmitter<NewsUpdate> {
51 publishNews(headline: string, content: string, category: NewsUpdate["category"]): void {
52 const news: NewsUpdate = {
53 headline,
54 content,
55 category,
56 timestamp: new Date()
57 };
58 this.notify(news);
59 }
60}
61
62// Usage
63const publisher = new NewsPublisher();
64const subscriber1 = new NewsSubscriber("Alice", ["tech", "sports"]);
65const subscriber2 = new NewsSubscriber("Bob", ["politics", "entertainment"]);
66
67publisher.attach(subscriber1);
68publisher.attach(subscriber2);
69
70publisher.publishNews("New iPhone Released", "Apple announces...", "tech");
71publisher.publishNews("Championship Game Tonight", "Sports update...", "sports");Decorator Pattern with Method Decorators:
1// Method decorator for logging
2function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
3 const method = descriptor.value;
4
5 descriptor.value = function (...args: any[]) {
6 console.log(`Calling ${propertyName} with arguments: ${JSON.stringify(args)}`);
7 const result = method.apply(this, args);
8 console.log(`${propertyName} returned: ${JSON.stringify(result)}`);
9 return result;
10 };
11}
12
13// Performance measurement decorator
14function measure(target: any, propertyName: string, descriptor: PropertyDescriptor) {
15 const method = descriptor.value;
16
17 descriptor.value = function (...args: any[]) {
18 const start = performance.now();
19 const result = method.apply(this, args);
20 const end = performance.now();
21 console.log(`${propertyName} took ${(end - start).toFixed(2)}ms`);
22 return result;
23 };
24}
25
26class Calculator {
27 @log
28 @measure
29 public add(a: number, b: number): number {
30 return a + b;
31 }
32
33 @log
34 @measure
35 public multiply(a: number, b: number): number {
36 return a * b;
37 }
38
39 @measure
40 public fibonacci(n: number): number {
41 if (n <= 1) return n;
42 return this.fibonacci(n - 1) + this.fibonacci(n - 2);
43 }
44}
45
46// Usage
47const calc = new Calculator();
48calc.add(5, 3);
49calc.multiply(4, 6);
50calc.fibonacci(10);Mixins for Multiple Inheritance:
1// Mixin types
2type Constructor<T = {}> = new (...args: any[]) => T;
3
4// Mixin functions
5function Timestamped<TBase extends Constructor>(Base: TBase) {
6 return class extends Base {
7 timestamp = new Date();
8
9 getTimestamp(): Date {
10 return this.timestamp;
11 }
12 };
13}
14
15function Activatable<TBase extends Constructor>(Base: TBase) {
16 return class extends Base {
17 isActive = false;
18
19 activate(): void {
20 this.isActive = true;
21 }
22
23 deactivate(): void {
24 this.isActive = false;
25 }
26 };
27}
28
29// Base class
30class User {
31 constructor(public name: string) {}
32}
33
34// Create class with mixins
35const TimestampedUser = Timestamped(User);
36const ActivatableUser = Activatable(User);
37const TimestampedActivatableUser = Timestamped(Activatable(User));
38
39// Usage
40const user = new TimestampedActivatableUser("John");
41user.activate();
42console.log(user.name); // "John"
43console.log(user.isActive); // true
44console.log(user.getTimestamp()); // Current dateYour progress is saved in this browser, on this device only.