Introduction to Generics

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.

Code examples

Basic generic functions

// 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.

Generic constraints with extends

// 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 utility functions

// 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.

Key points

Concepts covered

Generic functions, Type parameters, Generic constraints, Type argument inference