Generics in JavaScript: Why They Are Needed and How to Use Them
Modern development in JavaScript and Node.js requires not only fast code writing but also its reliability. One of the key tools that provide flexibility and type safety are Generics. In this article, we will break down what generics are, why they are needed in the JavaScript ecosystem, and how to effectively apply them in practice. The material will be useful for both beginners and experienced developers looking to deepen their knowledge.
What Are Generics and Why Are They Needed?
Generics are a mechanism that allows you to create components (functions, classes, interfaces) that work with different data types while maintaining strict typing. Unlike regular JavaScript, which is dynamically typed, generics come from the world of TypeScript — a superset of JS that adds static typing.
Why is this needed? Imagine you are writing a function that returns the first element of an array. Without generics, you would have to use the any type, which deprives you of IDE hints and increases the risk of errors. With Generics, you can write universal code that safely works with numbers, strings, objects, and any other types.
// Without generics (unsafe)function getFirst(arr: any[]): any { return arr[0];}
// With generics (safe)function getFirst<T>(arr: T[]): T | undefined { return arr[0];}
const num = getFirst([1, 2, 3]); // type numberconst str = getFirst(['a', 'b']); // type stringThus, generics make code more reusable and type-safe, which is especially important in large Node.js projects.
How to Use Generics in TypeScript (JavaScript with Types)
Although pure JavaScript does not support generics, TypeScript fully integrates with the Node.js ecosystem. Let's look at the main usage scenarios.
1. Generic Functions
The simplest way is to parameterize a function with one or more types. This is useful for utilities that work with collections.
// Function to merge two arraysfunction mergeArrays<T>(arr1: T[], arr2: T[]): T[] { return [...arr1, ...arr2];}
const numbers = mergeArrays([1, 2], [3, 4]); // number[]const strings = mergeArrays(['hello'], ['world']); // string[]Here <T> is a type parameter that is substituted automatically when the function is called. You can also explicitly specify the type:
const mixed = mergeArrays<number | string>([1], ['a']); // (number | string)[]2. Generic Classes
Generics are great for creating data structures such as stacks, queues, or caches.
class Stack<T> { private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }}
// Usageconst numberStack = new Stack<number>();numberStack.push(10);numberStack.push(20);console.log(numberStack.pop()); // 20
const stringStack = new Stack<string>();stringStack.push('TypeScript');console.log(stringStack.peek()); // 'TypeScript'3. Type Constraints
Sometimes you need to restrict a generic to a specific set of types. For example, guarantee that an object has a length property.
// Constrain T to types that have lengthfunction logLength<T extends { length: number }>(item: T): void { console.log(`Length: ${item.length}`);}
logLength('Hello'); // 5logLength([1, 2, 3]); // 3// logLength(123); // Error: number does not have lengthThe keyword exten