Introduction: What Are Generics and Why They Are Important
TypeScript is a strictly typed language that adds a powerful type system to JavaScript. However, strict typing can sometimes hinder writing flexible and reusable code. This is where Generics come to the rescue.
Generics are a tool that allows you to create components (functions, classes, interfaces) that work with different data types without losing type control. Imagine you are writing a function that should work with numbers, strings, and arrays. Without generics, you would have to either use any (which defeats the purpose of typing) or create separate functions for each data type. Generics solve this problem elegantly and safely.
In this article, we will explore why generics are needed in TypeScript and, most importantly, how to use them in practice — from simple functions to complex classes and type constraints.
Main Part
1. Generics in Functions: From Theory to Practice
The simplest and most common use case for generics is functions that accept and return data of the same type. Let's look at a classic task: a function that returns the first element of an array.
Bad approach (without Generics):
// Example with any — we lose type informationfunction getFirst(arr: any[]): any { return arr[0];}
const num = getFirst([1, 2, 3]); // type of num is any, we don't know it's a numberCorrect approach (with Generics):
// Using a generic type Tfunction getFirst<T>(arr: T[]): T { return arr[0];}
const num = getFirst([1, 2, 3]); // type of num is numberconst str = getFirst(['a', 'b', 'c']); // type of str is stringconst bool = getFirst([true, false]); // type of bool is booleanHow does this work? We declare a type parameter <T> (the name can be anything, but by convention T, U, V are used). TypeScript automatically infers the specific type based on the passed argument. This is called type inference.
You can also explicitly specify the type when calling the function:
const num = getFirst<number>([1, 2, 3]); // explicit type specificationAnother useful example is a function that returns a random element from an array:
function getRandom<T>(items: T[]): T { const randomIndex = Math.floor(Math.random() * items.length); return items[randomIndex];}
const randomNumber = getRandom([10, 20, 30]); // type is numberconst randomName = getRandom(['Anna', 'Ivan', 'Maria']); // type is string2. Generics in Interfaces and Types
Generics work great with interfaces as well. This allows you to create flexible data structures that can store values of different types.
Imagine we want to describe an API response that can contain data of any type:
interface ApiResponse<T> { status: number; message: string; data: T;}
// Usage for different data typesconst userResponse: ApiResponse<{ id: number; name: string }> = { status: 200, message: 'Success', data: { id: 1, name: 'Alexey' }};
const listResponse: ApiResponse<string[]> = { status: 200, message: 'List received', data: ['apple', 'banana', 'orange']};You can also use generics with type aliases (type):
type Result<T> = { success: boolean; value: T; error?: string;};
const successResult: Result<number> = { success: true, value: 42 };const errorResult: Result<string> = { success: false, error: 'Loading error', value: '' };Note: we can co