173 – Generics in TypeScript (Javascript)

TypeScript – Generics in TypeScript

Generics in TypeScript are a powerful feature that allows you to write flexible and reusable code. They enable you to create components and functions that can work with various data types while maintaining type safety. In this guide, we’ll explore what generics are and how to use them effectively in TypeScript.

1. Understanding Generics

Generics provide a way to define functions, classes, or interfaces that work with different data types while preserving type information. They allow you to create highly reusable code components that maintain type safety.

2. Creating a Generic Function

Let’s start with a simple example of a generic function that swaps the values of two variables:


function swap<T>(a: T, b: T): [T, T] {
  return [b, a];
}

const result = swap(5, 10);
console.log(result); // Output: [10, 5]

In this example, the `swap` function uses the generic type parameter `T`. This function can work with different data types, such as numbers, strings, or custom objects.

3. Using Generics in Classes

Generics are not limited to functions. You can also use them with classes to create data structures that adapt to different types. Here’s an example of a generic stack:


class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // Output: 2

const stringStack = new Stack<string>();
stringStack.push("Hello");
stringStack.push("World");
console.log(stringStack.pop()); // Output: "World"

Using generics with classes allows you to create type-safe data structures that work with various data types.

4. Constraining Generic Types

Sometimes, you may want to restrict the types that can be used with a generic function or class. You can do this by specifying constraints. For example, you can create a function that works only with types that have a `length` property:


function printLength<T extends { length: number }>(item: T): void {
  console.log(`Length: ${item.length}`);
}

printLength("Hello, TypeScript"); // Output: Length: 16
printLength([1, 2, 3, 4, 5]); // Output: Length: 5
printLength(42); // Error: Property 'length' does not exist on type '42'.

In this example, the `T` type is constrained to objects with a `length` property. This constraint ensures that you can safely access the `length` property without runtime errors.

5. Generic Interfaces

You can also use generics with interfaces to define contracts that work with various data types. Here’s an example of a generic interface for a repository:


interface Repository<T> {
  getById(id: number): T | undefined;
  save(item: T): void;
}

class UserRepository implements Repository<string> {
  private data: { [key: number]: string } = {};

  getById(id: number): string | undefined {
    return this.data[id];
  }

  save(item: string): void {
    this.data[new Date().getTime()] = item;
  }
}

const userRepo = new UserRepository();
userRepo.save("Alice");
console.log(userRepo.getById(1)); // Output: Alice

Generic interfaces define the structure of classes that can work with various data types, providing great flexibility and reusability.

Conclusion

Generics in TypeScript are a valuable tool for creating versatile and type-safe code components. They allow you to write functions, classes, and interfaces that work with different data types, ensuring your code remains maintainable and robust.