
TypeScript Beyond Basics
} -->
In software development, design patterns are proven solutions to common problems. They’re like blueprints created by experienced developers to solve recurring challenges - so you don’t have to reinvent the wheel every time.
Here’s why I always keep them in mind when building both frontend and backend code:
Ensures a class has only one instance and provides a global point of access to it.
Use case: Managing a centralized store or configuration.
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(msg: string) {
console.log(msg);
}
}
The Observer pattern, also known as Publish-Subscribe (Pub/Sub), is a behavioral design pattern that allows one object (the subject) to maintain a list of dependents (the observers) and automatically notify them of any state changes, usually by calling one of their methods.
This pattern is incredibly useful for decoupling components or modules, enabling them to communicate without tight dependencies.
Here’s a minimal implementation of a Pub/Sub system:
class EventBus {
constructor() {
this.events = {};
}
subscribe(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
unsubscribe(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
publish(eventName, data) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(callback => callback(data));
}
}
Usage:
const bus = new EventBus();
function onUserLogin(data) {
console.log(`User logged in: ${data.name}`);
}
// Subscribe to event
bus.subscribe('user:login', onUserLogin);
// Publish event somewhere in your app
bus.publish('user:login', { name: 'Bartek' });
// Later, unsubscribe if needed
bus.unsubscribe('user:login', onUserLogin);
Vue’s reactivity system is a built-in observer pattern under the hood. When reactive data changes, Vue automatically updates the DOM or triggers watchers.
import { reactive, watch } from 'vue';
const state = reactive({
count: 0,
});
watch(() => state.count, (newVal) => {
console.log('Count changed:', newVal);
});
state.count++; // This triggers the watcher, logging "Count changed: 1"
The Observer pattern helps organize communication in your apps cleanly and flexibly. It’s a must-know, especially for frontend developers dealing with event-driven UIs or backend developers building event systems.
The Factory pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. In simpler terms, it lets you create objects without exposing the instantiation logic to the client, and you can decide which object type to create at runtime.
Suppose you want to create different types of notifications: EmailNotification
, SMSNotification
, or PushNotification
. The Factory decides which one to instantiate based on input:
class EmailNotification {
send(message) {
console.log(`Sending email: ${message}`);
}
}
class SMSNotification {
send(message) {
console.log(`Sending SMS: ${message}`);
}
}
class PushNotification {
send(message) {
console.log(`Sending push notification: ${message}`);
}
}
class NotificationFactory {
static create(type) {
switch(type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
case 'push':
return new PushNotification();
default:
throw new Error('Unknown notification type');
}
}
}
// Usage:
const notification = NotificationFactory.create('sms');
notification.send('Hello from Factory!');
You can use Factory pattern to render different components based on props:
const components = {
primary: (props) => <PrimaryButton {...props} />,
secondary: (props) => <SecondaryButton {...props} />,
};
function ButtonFactory({ variant, ...props }) {
const Component = components[variant] || components.primary;
return <Component {...props} />;
}
// Usage
<ButtonFactory variant="secondary" onClick={() => alert('Clicked!')} />;
Factory pattern is a powerful way to centralize and organize object/component creation. It helps keep your codebase scalable and easy to maintain when you have many related objects or components with similar interfaces.
The Strategy pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
In practice, you create different strategies (algorithms) that share the same interface, and at runtime you decide which strategy to use.
if-else
or switch
).Imagine you have different sorting algorithms and want to switch between them:
class BubbleSortStrategy {
sort(data) {
// Simple bubble sort implementation
const arr = [...data];
for(let i = 0; i < arr.length; i++) {
for(let j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSortStrategy {
sort(data) {
if(data.length < 2) return data;
const pivot = data[0];
const lesser = data.slice(1).filter(x => x <= pivot);
const greater = data.slice(1).filter(x => x > pivot);
return [...this.sort(lesser), pivot, ...this.sort(greater)];
}
}
class SortContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(data) {
return this.strategy.sort(data);
}
}
// Usage:
const data = [5, 3, 8, 1, 2];
const context = new SortContext(new BubbleSortStrategy());
console.log('Bubble Sort:', context.sort(data));
context.setStrategy(new QuickSortStrategy());
console.log('Quick Sort:', context.sort(data));
You can use Strategy to swap validation rules dynamically:
const validators = {
email: (value) => /\S+@\S+\.\S+/.test(value) ? null : 'Invalid email',
password: (value) => value.length >= 6 ? null : 'Password too short',
};
function validateField(type, value) {
const validate = validators[type];
return validate ? validate(value) : null;
}
// Usage in a form component
const error = validateField('email', 'test@example.com');
The Strategy pattern helps you manage interchangeable algorithms cleanly and flexibly. Instead of hardcoding logic, you can swap behavior easily, which makes your code more modular and maintainable.
The Decorator pattern is a structural design pattern that allows you to add behavior to objects dynamically without altering their original structure. Instead of modifying the original object, you wrap it with a decorator that adds new functionality.
Imagine we have a simple logger, and we want to add timestamp functionality without modifying the original logger:
class Logger {
log(message) {
console.log(message);
}
}
class TimestampLogger {
constructor(logger) {
this.logger = logger;
}
log(message) {
const timestamp = new Date().toISOString();
this.logger.log(`[${timestamp}] ${message}`);
}
}
// Usage:
const logger = new Logger();
const decoratedLogger = new TimestampLogger(logger);
logger.log("Hello!"); // Output: Hello!
decoratedLogger.log("Hello!"); // Output: [2025-07-04T10:00:00.000Z] Hello!
In React, Higher-Order Components (HOCs) are a common way to decorate components:
function withLogger(WrappedComponent) {
return function(props) {
console.log('Rendering component with props:', props);
return <WrappedComponent {...props} />;
};
}
// Usage:
const Button = (props) => <button {...props}>Click me</button>;
const ButtonWithLogger = withLogger(Button);
Here, withLogger adds logging behavior without modifying Button.
Decorator pattern lets you add responsibilities to objects or components dynamically and transparently. It’s perfect when you want to keep your original code clean but still add extra functionality.
Design patterns are not strict rules - they’re tools. Knowing them lets you pick the right approach faster and communicate better with your team.
Remember: someone already solved your problem elegantly - why not use that solution?
Bartłomiej Nowak
Programmer
Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.