Generics in TypeScript: A Complete Guide to Generics

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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 silent


Here 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 number


The 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 string


Here 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

Blogs

Book Recommendations