Difficulty: Intermediate
Generics are one of TypeScript's most powerful features, allowing you to write functions, classes, and types that work with any type while still maintaining type safety. Instead of using `any` (which discards type information) or writing separate functions for each type, generics let you create a single function that preserves the relationship between input and output types. The syntax uses angle brackets with a type parameter: `function identity<T>(value: T): T`.
When you call a generic function, TypeScript infers the type parameter from the arguments you pass. If you call `identity("hello")`, TypeScript infers `T` as `string` and knows the return type is `string`. You can also provide the type parameter explicitly: `identity<number>(42)`. Type inference makes generics feel natural - most of the time, you don't need to specify type parameters manually because TypeScript figures them out from context.
Generic constraints restrict what types can be used with a type parameter. The `extends` keyword limits the type parameter to types that have certain properties. For example, `function getLength<T extends { length: number }>(item: T): number` ensures that only types with a `length` property can be passed. Without the constraint, TypeScript wouldn't know that `T` has a `length` property and would show an error when you try to access it.
Generics are pervasive in TypeScript's standard library. Arrays are generic (`Array<T>`), Promises are generic (`Promise<T>`), Maps are generic (`Map<K, V>`), and many utility types are generic (`Partial<T>`, `Required<T>`, `Pick<T, K>`). Understanding generics is essential for effectively using these built-in types and for writing your own reusable, type-safe abstractions.
// A generic identity function
function identity<T>(value: T): T {
return value;
}
// TypeScript infers the type parameter
const str = identity("hello"); // T inferred as string
const num = identity(42); // T inferred as number
const arr = identity([1, 2, 3]); // T inferred as number[]
console.log(str.toUpperCase()); // TypeScript knows it's a string
console.log(num.toFixed(1)); // TypeScript knows it's a number
console.log(arr.length); // TypeScript knows it's number[]
The generic type parameter `T` captures the input type and carries it through to the return type. TypeScript infers `T` automatically, so the return value has the correct specific type.
// Constraint: T must have a length property
function logLength<T extends { length: number }>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}
// Works with strings, arrays, and any object with .length
logLength("hello");
logLength([1, 2, 3, 4]);
logLength({ length: 10, name: "test" });
// logLength(42); // Error: number doesn't have .length
The constraint `T extends { length: number }` ensures T has a `length` property. Any type with that property works - strings, arrays, or custom objects. Types without `length` are rejected at compile time.
// Generic pair creator
function makePair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
// Generic array wrapper
function wrapInArray<T>(value: T): T[] {
return [value];
}
// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const pair = makePair("hello", 42);
console.log(pair);
const wrapped = wrapInArray({ x: 1, y: 2 });
console.log(wrapped);
const user = { name: "Alice", age: 30 };
console.log(getProperty(user, "name"));
Multiple type parameters (A, B) capture different types. The `keyof` constraint on `getProperty` ensures you can only access keys that exist on the object, and the return type matches the property's type.
Generic functions, Type parameters, Generic constraints, Type argument inference