Generics in TypeScript: Why They're Needed and How to Use Them
TypeScript is a powerful tool that adds static typing to JavaScript. However, if you are just starting to learn TypeScript, you may have encountered a situation where you have to write the same code for different data types. This is where Generics come to the rescue. In this article, we will break down what Generics are, why they are needed, and how to use them in practice.
What are Generics and Why Are They Important?
Generics are a TypeScript mechanism that allows you to create components (functions, classes, interfaces) that work with various data types, rather than just one specific type. Imagine you are writing a function that needs to work with numbers, strings, and arrays. Without generics, you would either have to use the any type (which defeats the whole purpose of TypeScript) or create several copies of the same function.
The main advantages of Generics:
- Type Safety — you maintain strict typing while keeping the code flexible.
- Code Reusability — one function or class can work with different types.
- Readability — the code becomes more understandable and self-documenting.
Basic Generics Syntax
Let's start with the simplest example. Suppose we need a function that returns the argument passed to it. Without Generics, we would write it like this:
function identity(arg: number): number { return arg;}But what if we need to pass a string? We would have to create a new function. With Generics, we do it like this:
function identity<T>(arg: T): T { return arg;}
// Usage:let output1 = identity<string>("Hello");let output2 = identity<number>(42);let output3 = identity<boolean>(true);Here, <T> is a type parameter (or type variable). When we call the function, TypeScript automatically substitutes the required type. You can use any letter: T, U, V, or even meaningful names like Type.
By the way, TypeScript can infer the type itself, so you can often write without explicitly specifying it:
let output = identity("Hello"); // TypeScript will understand that T = stringWorking with Arrays and Multiple Types
One of the most common uses of Generics is working with arrays. Suppose we want to write a function that takes an array and returns the first element:
function getFirstElement<T>(arr: T[]): T | undefined { return arr[0];}
const numbers = [1, 2, 3];const firstNumber = getFirstElement(numbers); // type number | undefined
const strings = ["a", "b", "c"];const firstString = getFirstElement(strings); // type string | undefinedNotice: we used T[] — this is the same as Array<T>. And what if the function takes two different types? Easy!
function merge<T, U>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 };}
const result = merge({ name: "Anna" }, { age: 25 });// result has type { name: string; } & { age: number; }// Effectively: { name: string; age: number; }Generics Constraints
Sometimes you need a generic to work not with any type, but only with those that have certain properties. For this, constraints are used with the extends keyword.
For example, we want to write a function that gets the length of an object. Obviously, not all types have a length property. Let's check this:
function getLength<T extends { length: number