8/15 lessons53%

Generics in TypeScript

TypeScript generics are like flexible templates that let you write reusable code while keeping full type safety. Instead of locking your functions and classes to specific types, generics allow them to work with different types while still catching errors during development. This powerful feature helps you build more adaptable and maintainable code.

Understanding Generics and Why They Matter

Imagine you're writing a function that returns whatever input it receives. Without generics, you'd have to write separate functions for strings, numbers, and other types - or use any, which loses type safety. Generics solve this by letting you create a "type placeholder" that gets filled in when the code is used.

Here's a simple example showing the problem and how generics help:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
          // Without generics (limited or unsafe)
function identityAny(arg: any): any {
  return arg; // We lose type information
}

// With generics (type-safe and flexible)
function identity<T>(arg: T): T {
  return arg; // Keeps the exact type
}

// Usage
let output1 = identity<string>("hello"); // Type: string
let output2 = identity<number>(42);      // Type: number
let output3 = identity("TypeScript");    // Type inferred as string
        

Key benefits of generics:

  • Reusability: Write one function that works with multiple types
  • Type Safety: Maintain proper type checking throughout
  • Intellisense: Get full code completion in your editor
  • Avoid Any: Prevent the pitfalls of using any type

Writing Generic Functions and Classes

Generics work with both functions and classes, following similar principles. The angle brackets (<>) declare type parameters that get filled in when the code is used.

Generic Functions

You've seen a basic generic function. Let's explore more practical examples:

typescript
1
2
3
4
5
6
7
8
9
10
          // Generic function working with arrays
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

const numbers = [1, 2, 3];
const firstNum = getFirstElement(numbers); // Type: number

const strings = ["a", "b", "c"];
const firstStr = getFirstElement(strings); // Type: string
        

Generic Classes

Classes can also use generics to create flexible data structures:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
          class Box<T> {
  private content: T;

  constructor(value: T) {
    this.content = value;
  }

  getValue(): T {
    return this.content;
  }
}

// Usage
const stringBox = new Box("secret message");
console.log(stringBox.getValue()); // Type: string

const numberBox = new Box(100);
console.log(numberBox.getValue()); // Type: number
        

Multiple Type Parameters

Functions and classes can accept multiple generic types:

typescript
1
2
3
4
5
          function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const mixedPair = pair("hello", 42); // Type: [string, number]
        

Using Generics with Constraints

Sometimes you need to ensure generic types meet certain requirements. TypeScript lets you add constraints using the extends keyword.

Basic Constraints

typescript
1
2
3
4
5
6
7
8
9
10
11
          interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): void {
  console.log(arg.length);
}

logLength("hello");    // Works (strings have length)
logLength([1, 2, 3]);  // Works (arrays have length)
// logLength(42);      // Error: number doesn't have length
        

Using Type Parameters in Constraints

You can constrain one type parameter based on another:

typescript
1
2
3
4
5
6
7
          function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const person = { name: "Alice", age: 30 };
getProperty(person, "name"); // Works
// getProperty(person, "email"); // Error: "email" doesn't exist
        

Default Type Parameters

Generics can have default types when none are provided:

typescript
1
2
3
4
5
6
          function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const strArray = createArray(3, "x");  // string[]
const numArray = createArray<number>(3, 0);  // number[]
        

Real-World Use Cases

Generics shine in many practical scenarios:

1. API Response Handling

typescript
1
2
3
4
5
6
7
8
          interface ApiResponse<T> {
  data: T;
  status: number;
}

function fetchUser(): Promise<ApiResponse<User>> {
  // API call implementation
}
        

2. State Management

typescript
1
2
3
4
5
6
7
8
9
10
11
          class State<T> {
  constructor(private current: T) {}
  
  update(newValue: T) {
    this.current = newValue;
  }
  
  get(): T {
    return this.current;
  }
}
        

3. Higher-Order Functions

typescript
1
2
3
4
5
6
7
8
          function timedFunction<T extends (...args: any[]) => any>(fn: T): T {
  return function(...args: Parameters<T>): ReturnType<T> {
    console.time("Function timer");
    const result = fn(...args);
    console.timeEnd("Function timer");
    return result;
  } as T;
}
        

By mastering generics, you'll write TypeScript code that's both flexible and type-safe, reducing duplication while maintaining excellent code quality. The key is to start simple and gradually incorporate more advanced generic patterns as you become comfortable with the core concepts.