Mastering TypeScript Generics for Scalable and Reusable Code

Learn how TypeScript generics enable you to write scalable, flexible, and reusable code by allowing functions and classes to work with any data type.

TypeScript generics might sound complex at first, but once you understand the basics, they become a powerful tool for writing reusable and scalable code. Generics allow you to create components, functions, or classes that work with a variety of data types, without sacrificing type safety.

Imagine you want to create a function that returns the first element of an array. In JavaScript, you write it simply, but TypeScript needs to know what type of elements are inside the array to provide accurate type checking.

typescript
function getFirstElement(arr: any[]): any {
  return arr[0];
}

const num = getFirstElement([1, 2, 3]); // Type is any
const str = getFirstElement(["a", "b", "c"]); // Type is any

The above function works, but TypeScript does not know what type it returns because we used `any`. Using generics, we can specify that the function works for any type, and the return type will match the array's element type.

typescript
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

const num = getFirstElement([1, 2, 3]); // Type is number
const str = getFirstElement(["a", "b", "c"]); // Type is string

Here, `` is a placeholder for the type that will be provided when you call the function. TypeScript infers the type based on the argument you pass in. This makes the function reusable for any type, while still keeping strict typing.

Generics are not limited to functions. You can also use them with interfaces and classes to build flexible data structures.

typescript
interface Box<T> {
  value: T;
}

const numberBox: Box<number> = { value: 123 };
const stringBox: Box<string> = { value: "hello" };

In this example, the `Box` interface holds a single value of any type `T`. When creating instances, you specify the type you want it to hold, allowing TypeScript to enforce correct usage.

Similarly, classes can be made generic which is incredibly useful when implementing data structures like stacks, queues, or linked lists.

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

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

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

const numberStack = new Stack<number>();
numberStack.push(10);
const poppedNumber = numberStack.pop(); // number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");
const poppedString = stringStack.pop(); // string | undefined

By mastering generics, you enhance code reusability and maintain type safety. This helps reduce bugs and increases productivity, especially in large scale projects.

To summarize: use generics when you want your functions, interfaces, or classes to work with multiple data types. Let TypeScript infer the generic types whenever possible to keep your code clean and readable.