Introduction to Generics in TypeScript
TypeScript is a strictly typed language that extends JavaScript's capabilities by adding static typing. However, one of its most powerful and flexible features is the Generics mechanism. If you've ever written code that needs to work with different data types while maintaining type safety, then Generics are exactly what you need.
Imagine you're creating a universal function that can accept arrays of numbers, strings, or objects. Without generics, you would either have to use the any type (which defeats the purpose of TypeScript) or write separate functions for each type. Generics solve this problem elegantly and safely. In this article, we'll explore what Generics are, why they're needed, and how to use them effectively in real projects.
What are Generics and why are they needed?
Generics is a mechanism that allows you to create components (functions, classes, interfaces) capable of working with different data types without losing type information. Instead of fixing a specific type, you use a type parameter that gets substituted at the time of invocation.
The problem without Generics
Suppose you want to write a function that returns the first element of an array. Without generics, it might look like this:
function getFirstElement(arr: any[]): any { return arr[0];}
const num = getFirstElement([1, 2, 3]); // Type 'any' — we lost the informationconsole.log(num.toUpperCase()); // Runtime error, but TypeScript stays silentHere we use any, and TypeScript cannot warn us that num is a number, not a string. This leads to potential runtime errors.
Solution with Generics
Now let's rewrite the same function using generics:
function getFirstElement<T>(arr: T[]): T { return arr[0];}
const num = getFirstElement([1, 2, 3]); // Type 'number'const str = getFirstElement(['a', 'b', 'c']); // Type 'string'
// num.toUpperCase(); // Compilation error — TypeScript knows num is a numberThe symbol <T> is the type parameter. It acts as a placeholder: when we call the function with an array of numbers, T becomes number, and the return value also gets the type number. Thus, we maintain strict typing without losing flexibility.
How to use Generics: main scenarios
Generics are used not only in functions but also in classes, interfaces, and types. Let's look at the most common scenarios.
Generics in functions
The simplest and most frequent case is functions that work with data of different types. For example, a function for creating a key-value pair:
function createPair<K, V>(key: K, value: V): { key: K; value: V } { return { key, value };}
const pair = createPair('id', 123); // { key: string; value: number }console.log(pair.key.toUpperCase()); // Works// pair.key.toFixed(); // Error — key is a stringHere we use two type parameters: K for the key and V for the value. TypeScript automatically infers the types based on the arguments.
Generics in classes
Generics are great for creating universal classes such as stacks, queues, or data stores:
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]; }}
const numberStack = new Stack<number>();numberStack.push(10);numberStack.push(20);console.log(numberStack