Implementing a Queue in TypeScript
What is a Queue?
A queue is a data structure that stores elements using the FIFO (First In First Out) strategy for accessing methods.
Think of a queue as a line of people waiting for a service, like at a bank or a movie theater. The first person in line is the first one to be served (First-In-First-Out, or FIFO). This simple concept has numerous applications in computing.
Why Queues Matter
Understanding queues helps you grasp more complex data structures and algorithms. Learning queues enhances your programming skills, and implementing one from scratch gives you a deeper understanding of memory management and data organization.
Real-world applications of queues:
- Task scheduling - Operating systems use queues to manage processes
- Print spooling - Print jobs are queued and processed in order
- Breadth-First Search (BFS) - Graph traversal algorithms use queues
- Message queues - Services like RabbitMQ, Kafka handle asynchronous messaging
- Request handling - Web servers queue incoming requests
- Undo/Redo functionality - Combined with stacks for complex operations
Queue Operations
A queue typically supports three core operations:
- enqueue(item) - Add an item to the back of the queue
- dequeue() - Remove and return the front item
- peek() - View the front item without removing it
How to Implement a Queue in TypeScript
I’ll implement a queue using a linked list structure for optimal performance. Using a linked list allows O(1) time complexity for both enqueue and dequeue operations.
The Node Type
First, define a node structure that holds a value and points to the next node:
type Node<T> = {
value: T;
next?: Node<T>;
};
The Queue Class
Here’s a complete implementation:
export default class Queue<T> {
public length: number;
private head?: Node<T>;
private tail?: Node<T>;
constructor() {
this.head = this.tail = undefined;
this.length = 0;
}
// Add item to the back of the queue
enqueue(item: T): void {
const node = { value: item } as Node<T>;
this.length++;
// If queue is empty, new node is both head and tail
if (!this.tail) {
this.tail = this.head = node;
return;
}
// Link new node to current tail
this.tail.next = node;
// Update tail to new node
this.tail = node;
}
// Remove and return front item
deque(): T | undefined {
if (!this.head) {
return undefined;
}
this.length--;
// Save current head
const head = this.head;
// Move head to next node
this.head = this.head.next;
// Clean up removed node
head.next = undefined;
// If queue is now empty, clear tail
if (this.length === 0) {
this.tail = undefined;
}
return head.value;
}
// View front item without removing
peek(): T | undefined {
return this.head?.value;
}
}
How It Works
Enqueue (Adding items):
- Create a new node with the item
- If queue is empty, this node becomes both head and tail
- Otherwise, link it to the current tail and update the tail pointer
Dequeue (Removing items):
- If queue is empty, return undefined
- Save the head node’s value
- Move the head pointer to the next node
- Return the saved value
Peek (Viewing the front):
- Return the head’s value without modifying the queue
Complexity Analysis
- enqueue(): O(1) - Constant time, just updating tail pointer
- dequeue(): O(1) - Constant time, just updating head pointer
- peek(): O(1) - Constant time, just reading head value
- Space: O(n) - Where n is the number of elements in the queue
This is optimal for queue operations. Compare this to using an array where dequeue would be O(n) due to shifting elements.
Usage Examples
// Create a queue of numbers
const queue = new Queue<number>();
// Add items
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
console.log(queue.length); // 3
// View front item
console.log(queue.peek()); // 1
// Remove items
console.log(queue.deque()); // 1
console.log(queue.deque()); // 2
console.log(queue.length); // 1
// Queue of strings
const taskQueue = new Queue<string>();
taskQueue.enqueue("Process payment");
taskQueue.enqueue("Send email");
taskQueue.enqueue("Update inventory");
while (taskQueue.length > 0) {
const task = taskQueue.deque();
console.log(`Processing: ${task}`);
}
Real-World Example: Task Processor
Here’s how you might use a queue in a task processing system:
interface Task {
id: string;
description: string;
priority: number;
}
class TaskProcessor {
private queue = new Queue<Task>();
addTask(task: Task): void {
this.queue.enqueue(task);
console.log(`Task added: ${task.description}`);
}
processNext(): void {
const task = this.queue.deque();
if (task) {
console.log(`Processing task ${task.id}: ${task.description}`);
// Simulate processing
this.executeTask(task);
} else {
console.log("No tasks to process");
}
}
private executeTask(task: Task): void {
// Task execution logic here
}
getPendingCount(): number {
return this.queue.length;
}
}
// Usage
const processor = new TaskProcessor();
processor.addTask({ id: "1", description: "Send welcome email", priority: 1 });
processor.addTask({ id: "2", description: "Generate report", priority: 2 });
processor.processNext(); // Processes task 1 first (FIFO)
Key Takeaways
- Queues implement FIFO (First In First Out) ordering
- Linked list implementation gives O(1) for enqueue and dequeue
- Essential for BFS algorithms, task scheduling, and async processing
- TypeScript generics make the queue reusable for any type
- Understanding queues is fundamental for system design interviews
- Always maintain head and tail pointers for efficient operations
When to Use Queues
- When order of processing matters (FIFO)
- For breadth-first search in graphs or trees
- Managing asynchronous tasks
- Implementing buffers or caches
- Rate limiting and throttling requests
Related Articles
- How to Set Up Dynamic Imports in Laravel Mix and TypeScript - Advanced TypeScript configuration
- Visual Regression Testing with Playwright - Test your TypeScript applications