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?
- Encapsulation: Bundle data (attributes) and methods (functions) that operate on the data into a single unit.
- Inheritance: Create a new class based on an existing class, inheriting attributes and behaviors.
- Polymorphism: Allow objects of different classes to be treated as objects of a common super class.
- 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:
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:
- Tager en klasse som input (base class)
- Returnerer en ny klasse der udvider base class med ny funktionalitet
- 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