Generics are one of TypeScript's most powerful features. They allow you to write flexible, reusable code by using placeholders for types instead of fixing them to a specific one like string or number.
Essentially, generics allow a function, class, or interface to "capture" the type the user provides and apply it to the rest of the code block.
The Problem: Fixed Types
Without generics, you are forced to either write multiple versions of the same function or use the dangerous any type, which removes all the benefits of TypeScript.
Example (Only works for strings):
function echoString(value: string): string {
return value;
}
echoString("Hello"); // ✅ Works
echoString(123); // ❌ Error: Argument of type 'number' is not assignable to 'string'
The Solution: Generics (<T>)
By using the <T> syntax (where T stands for "Type"), you create a variable for the type itself. This placeholder is filled in at the moment the function is actually called.
Example (Works for anything):
function echo<T>(value: T): T {
return value;
}
// TypeScript "sees" the type and assigns it to T automatically
console.log(echo("Hello")); // T is inferred as string
console.log(echo(123)); // T is inferred as number
console.log(echo(true)); // T is inferred as boolean
Why Use Generics?
- Reusability: You don't need to write
echoString,echoNumber, andechoBoolean. One function handles all of them. - Type Safety: Unlike using
any, generics maintain a "link" between the input and the output. If you pass anumberin, TypeScript knows for a fact that anumberis coming out. - Flexibility: It allows your code to adapt to different data structures (like API responses or UI components) while keeping strict rules in place.
Common Generic Patterns
While T is the standard naming convention, you can use any name (like U or Value). You can also use multiple generics at once:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("Age", 25); // Returns a [string, number] tuple
Generic Constraints
By default, a generic accepts any type — which is sometimes too open. Generic constraints allow you to restrict what types a generic can accept, using the extends keyword. This gives you flexibility while still enforcing rules.
function getLength<T extends string | number>(value: T): number {
return value.toString().length;
}
getLength("Hello"); // ✅ Works — string is allowed
getLength(12345); // ✅ Works — number is allowed
getLength(true); // ❌ Error: Argument of type 'boolean' is not assignable to 'string | number'
You can also constrain a generic to types that have specific properties. For example, constraining to types that have a length property:
function logLength<T extends { length: number }>(value: T): void {
console.log(value.length);
}
logLength("Hello"); // ✅ 5 — strings have length
logLength([1, 2, 3]); // ✅ 3 — arrays have length
logLength(42); // ❌ Error: number does not have a length property
Constraints are the bridge between the full openness of an unconstrained generic and the rigidity of a fixed type — giving you the best of both.