Introduction: Why Does TypeScript Lose Half Its Power Without Generics?
Imagine you are an architect who designs a building from scratch every time, even if you just need to build another identical garage. That's roughly how a developer feels when writing the same function for numbers, strings, and arrays without using TypeScript generics. Sounds familiar? If you've already mastered basic types in TypeScript but feel you're writing too much duplicate code, this article is for you.
Generics are a mechanism that allows you to create components (functions, classes, interfaces) that work with different types, rather than a single specific one. Instead of copying code, you write it once, and the type is substituted automatically. In this article, we'll break down how generic programming works in TypeScript, show real code examples, and give practical tips that will make your code cleaner and safer.
We are PythonLib, and today we're talking about TypeScript because modern frontend development is unthinkable without it. Let's go!
What Are Generics in TypeScript and Why Do You Need Them?
Let's start with a simple example. Suppose you want to write a function that returns the argument passed to it (an identity function).
// Without generics — you have to use anyfunction identity(arg: any): any { return arg;}
// Or write overloads for each typefunction identityNumber(arg: number): number { return arg; }function identityString(arg: string): string { return arg; }The first option kills typing — you lose type information. The second forces you to create a bunch of identical functions. TypeScript generics solve this problem elegantly:
function identity<T>(arg: T): T { return arg;}
const num = identity<number>(42); // type numberconst str = identity('hello'); // type string (type inference)const bool = identity(true); // type booleanThe symbol <T> is a type parameter. You can name it anything (commonly T, U, V are used), but it's better to give meaningful names if there are many. When you call the function, TypeScript substitutes the real type for T. If the type is not explicitly specified, the compiler will infer it automatically from the argument.
Thus, generic types allow you to maintain strict typing where you previously had to use any. This reduces the number of bugs and makes the code self-documenting.
Generics Syntax: From Simple to Complex
Generic Functions
We've already seen a basic example. Let's make it more complex: a function that takes an array and returns its first element.
function getFirst<T>(arr: T[]): T | undefined { return arr[0];}
const firstNumber = getFirst([1, 2, 3]); // number | undefinedconst firstString = getFirst(['a', 'b', 'c']); // string | undefinedNote: T[] is shorthand for Array<T>. You can use either form.
Generic Interfaces
Interfaces with generics are a powerful tool for describing data structures. For example, a typed API response:
interface ApiResponse<T> { data: T; status: number; message: string;}
// Usageconst userResponse: ApiResponse<{ id: number; name: string }> = { data: { id: 1, name: 'Alice' }, status: 200, message: 'OK'};Now one interface can describe responses for users, products, orders — just substitute the required type.
Generic Classes
Classes can also be generic. A classic example is a Queue:
c