Introduction to Generics
Generics allow you to write reusable code that works with multiple types while maintaining type safety. Think of them as type parameters.
// Without generics - loses type information
function identity(arg: any): any {
return arg;
}
const result = identity("hello"); // type: any (lost!)
// With generics - preserves type information
function identity<T>(arg: T): T {
return arg;
}
const str = identity<string>("hello"); // type: string
const num = identity<number>(42); // type: number
const inferred = identity("world"); // TypeScript infers T = string
// Generic array function
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const first = firstElement([1, 2, 3]); // type: number
const name = firstElement(["Alice", "Bob"]); // type: string
// Multiple type parameters
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("name", 42); // type: [string, number]
Generic Constraints
Constraints limit what types can be passed to a generic, allowing you to access specific properties safely.
// Constraint: T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("alice", "bob"); // string
longest([1, 2, 3], [4, 5]); // number[]
longest({ length: 3 }, { length: 5, value: "x" }); // object
// longest(1, 2); // Error: number doesn't have length
// Constraint: K must be a key of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 25, active: true };
const userName = getProperty(user, "name"); // type: string
const userAge = getProperty(user, "age"); // type: number
// getProperty(user, "invalid"); // Error: not a valid key
// Conditional constraint
type NonNullableType<T> = T extends null | undefined ? never : T;
type A = NonNullableType<string | null>; // string
type B = NonNullableType<number | undefined>; // number
type C = NonNullableType<null>; // never
Generic Classes & Interfaces
Classes and interfaces can also be generic, enabling powerful data structure implementations.
// Generic Stack data structure
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
const top = numberStack.pop(); // type: number | undefined
// Generic Result type (like Rust's Result)
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: "Cannot divide by zero" };
return { ok: true, value: a / b };
}
const result = divide(10, 2);
if (result.ok) {
console.log(result.value); // type: number
} else {
console.error(result.error); // type: string
}
Generics are the foundation of type-safe utility libraries. Understanding them unlocks the full power of TypeScript.