Skip to content

Classes

In TypeScript, Classes encapsulate data for the object and methods to manipulate that data. They are a fundamental building block of object-oriented programming (OOP) and bring structure and strong typing to your TypeScript applications.

Why Use Classes?

  1. Encapsulation: Bundle data (attributes) and methods (functions) that operate on the data into a single unit.
  2. Inheritance: Create a new class based on an existing class, inheriting attributes and behaviors.
  3. Polymorphism: Allow objects of different classes to be treated as objects of a common super class.
  4. Strong Typing: Ensure that objects are created and used according to a specific blueprint.

Defining a Class

To define a class, use the class keyword:

class Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    move(distance: number) {
        console.log(`${this.name} moved ${distance} meters.`);
    }
}

Instantiating Classes

Create an instance of a class using the new keyword:

let dog = new Animal('Dog');
dog.move(10);

Access Modifiers

TypeScript supports three access modifiers:

  • public: Accessible from any location (default).
  • private: Accessible only within the class.
  • protected: Accessible within the class and its subclasses.
class Employee {
    private empCode: number;
    empName: string; // public by default

    constructor(code: number, name: string) {
        this.empCode = code;
        this.empName = name;
    }
}

Inheritance

Classes can inherit properties and methods from another class using the extends keyword:

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

let dog = new Dog('Rover');
dog.bark();
dog.move(20);

Getters and Setters

Use getters and setters to control the access and modification of class attributes:

class Circle {
    private _radius: number;

    constructor(radius: number) {
        this._radius = radius;
    }

    get radius() {
        return this._radius;
    }

    set radius(value: number) {
        if (value < 0) {
            throw new Error('Radius cannot be negative.');
        }
        this._radius = value;
    }
}

Static Members

Static members belong to the class itself rather than any specific instance:

class Grid {
    static origin = { x: 0, y: 0 };

    calculateDistanceFromOrigin(point: { x: number; y: number }) {
        let xDist = point.x - Grid.origin.x;
        let yDist = point.y - Grid.origin.y;
        return Math.sqrt(xDist * xDist + yDist * yDist);
    }
}

Abstract Classes

Abstract classes are base classes that cannot be instantiated. They can, however, be extended:

abstract class Shape {
    abstract area(): number;
}

class Rectangle extends Shape {
    constructor(public width: number, public height: number) {
        super();
    }

    area() {
        return this.width * this.height;
    }
}

Advanced Topics

### Readonly Properties

Readonly properties can only be assigned during initialization or in the constructor. This ensures immutability after object creation.

class Person {
    readonly birthDate: Date;
    readonly ssn: string;

    constructor(birthDate: Date, ssn: string) {
        this.birthDate = birthDate;
        this.ssn = ssn;
    }

    // This would cause an error:
    // changeBirthDate(newDate: Date) {
    //     this.birthDate = newDate; // Error: Cannot assign to 'birthDate'
    // }
}

const person = new Person(new Date('1990-01-01'), '123-45-6789');
// person.birthDate = new Date(); // Error: Cannot assign to 'birthDate'

Parameter Properties

Parameter properties allow you to create and initialize class members in one place, directly in the constructor parameters.

// Traditional approach
class Product1 {
    name: string;
    price: number;

    constructor(name: string, price: number) {
        this.name = name;
        this.price = price;
    }
}

// Parameter properties (shorthand)
class Product2 {
    constructor(
        public name: string,
        public price: number,
        private id: string = crypto.randomUUID()
    ) {}
}

const product = new Product2('Laptop', 999);
console.log(product.name);   // 'Laptop'
console.log(product.price);  // 999
// console.log(product.id);  // Error: Property 'id' is private

Method Overloading

TypeScript supports method overloading through function signatures, allowing the same method to handle different parameter types.

class Calculator {
    // Overload signatures
    add(a: number, b: number): number;
    add(a: string, b: string): string;
    add(a: number[], b: number[]): number[];

    // Implementation signature
    add(a: any, b: any): any {
        if (typeof a === 'number' && typeof b === 'number') {
            return a + b;
        }
        if (typeof a === 'string' && typeof b === 'string') {
            return a + b;
        }
        if (Array.isArray(a) && Array.isArray(b)) {
            return [...a, ...b];
        }
        throw new Error('Invalid arguments');
    }
}

const calc = new Calculator();
console.log(calc.add(5, 3));           // 8
console.log(calc.add('Hello', ' World')); // 'Hello World'
console.log(calc.add([1, 2], [3, 4]));   // [1, 2, 3, 4]

Mixins

Mixins er en kraftfuld teknik til at genbruge funktionalitet på tværs af klasser uden at bruge traditionel arv. I stedet for at en klasse kun kan arve fra én superklasse (som i TypeScript/JavaScript), kan mixins tilføje flere forskellige funktionaliteter til en klasse.

Et mixin er en funktion der:

  1. Tager en klasse som input (base class)
  2. Returnerer en ny klasse der udvider base class med ny funktionalitet
  3. Kan kombineres med andre mixins for at sammensætte kompleks funktionalitet

Simpelt eksempel

// Base class
class Animal {
    constructor(public name: string) {}
}

// Mixin der tilføjer "jump" funktionalitet
function Jumpable<T extends new (...args: any[]) => {}>(Base: T) {
    return class extends Base {
        jump() {
            console.log(`${(this as any).name} is jumping!`);
        }
    };
}

// Mixin der tilføjer "swim" funktionalitet
function Swimmable<T extends new (...args: any[]) => {}>(Base: T) {
    return class extends Base {
        swim() {
            console.log(`${(this as any).name} is swimming!`);
        }
    };
}

// Brug mixins til at skabe nye klasser
const JumpingAnimal = Jumpable(Animal);
const SwimmingAnimal = Swimmable(Animal);
const SuperAnimal = Jumpable(Swimmable(Animal)); // Kombiner flere mixins!

// Test
const frog = new SuperAnimal('Frog');
frog.jump();  // Frog is jumping!
frog.swim();  // Frog is swimming!

Mere praktisk eksempel

// Base class
class Product {
    constructor(public name: string, public price: number) {}
}

// Mixin: Tilføj discountable funktionalitet
function Discountable<T extends new (...args: any[]) => {}>(Base: T) {
    return class extends Base {
        discount: number = 0;

        applyDiscount(percentage: number) {
            this.discount = percentage;
            console.log(`${percentage}% discount applied`);
        }

        getFinalPrice(): number {
            const p = (this as any).price;
            return p - (p * this.discount / 100);
        }
    };
}

// Mixin: Tilføj trackable funktionalitet
function Trackable<T extends new (...args: any[]) => {}>(Base: T) {
    return class extends Base {
        views: number = 0;

        incrementViews() {
            this.views++;
        }

        getViews(): number {
            return this.views;
        }
    };
}

// Kombiner mixins
const AdvancedProduct = Trackable(Discountable(Product));

// Brug
const laptop = new AdvancedProduct('Laptop', 1000);
laptop.applyDiscount(10);        // 10% discount applied
console.log(laptop.getFinalPrice()); // 900

laptop.incrementViews();
laptop.incrementViews();
console.log(laptop.getViews());  // 2

Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it.

class Database {
    private static instance: Database;
    private connections: number = 0;

    // Private constructor prevents instantiation from outside
    private constructor() {
        console.log('Database instance created');
    }

    public static getInstance(): Database {
        if (!Database.instance) {
            Database.instance = new Database();
        }
        return Database.instance;
    }

    public connect(): void {
        this.connections++;
        console.log(`Connected. Total connections: ${this.connections}`);
    }

    public getConnectionCount(): number {
        return this.connections;
    }
}

// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();

console.log(db1 === db2);  // true - same instance

db1.connect();  // Connected. Total connections: 1
db2.connect();  // Connected. Total connections: 2

Factory Pattern

The Factory pattern provides an interface for creating objects without specifying their exact classes.

// Product interfaces
interface Vehicle {
    type: string;
    wheels: number;
    drive(): void;
}

// Concrete products
class Car implements Vehicle {
    type = 'Car';
    wheels = 4;

    drive() {
        console.log('Driving a car');
    }
}

class Motorcycle implements Vehicle {
    type = 'Motorcycle';
    wheels = 2;

    drive() {
        console.log('Riding a motorcycle');
    }
}

class Truck implements Vehicle {
    type = 'Truck';
    wheels = 6;

    drive() {
        console.log('Driving a truck');
    }
}

// Factory
class VehicleFactory {
    static createVehicle(type: 'car' | 'motorcycle' | 'truck'): Vehicle {
        switch (type) {
            case 'car':
                return new Car();
            case 'motorcycle':
                return new Motorcycle();
            case 'truck':
                return new Truck();
            default:
                throw new Error('Unknown vehicle type');
        }
    }
}

// Usage
const car = VehicleFactory.createVehicle('car');
const motorcycle = VehicleFactory.createVehicle('motorcycle');

car.drive();        // Driving a car
motorcycle.drive(); // Riding a motorcycle

Class Decorators and Metadata

Decorators (when enabled) provide a way to add annotations and meta-programming syntax for class declarations and members.

// Note: Requires "experimentalDecorators": true in tsconfig.json

// Class decorator
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

// Method decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey} with args:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Result:`, result);
        return result;
    };

    return descriptor;
}

// Property decorator
function readonly(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
        writable: false
    });
}

@sealed
class Calculator {
    @readonly
    version = '1.0';

    @log
    add(a: number, b: number): number {
        return a + b;
    }
}

const calc = new Calculator();
console.log(calc.add(5, 3));  // Logs method call and result