Generics in TypeScript: Write Flexible and Type-Safe Code

đź‘€ Views 12.5K
🔄 Shares 847
⏰ Read time 7 min

Generics are a cornerstone of TypeScript, enabling developers to write flexible and reusable code. If you’ve ever found yourself writing similar functions or components that differ only in the types they handle, generics are your new best friend. In this post, we’ll explore the magic of generics through simple, engaging examples that will leave you wondering how you ever coded without them!

What Are Generics?

Generics allow you to create components or functions that work with any data type while maintaining type safety. Think of them as placeholders for types that are specified when the function or component is used. This means you can write code that is both flexible and type-safe.

The Problem Without Generics

Imagine you have a function that returns the first element of an array:

function getFirstElementString(arr: string[]): string {
  return arr[0];
}

function getFirstElementNumber(arr: number[]): number {
  return arr[0];
}

You’d need to write separate functions for each type—not ideal for maintainability or scalability.

The Solution With Generics

Generics solve this by allowing you to define a type parameter:

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

Here, T is a type parameter that gets replaced with the actual type when the function is called.

Getting Started with Generics

Basic Example: A Generic Identity Function

Let’s start with a simple example: a function that returns its input unchanged.

function identity<T>(arg: T): T {
  return arg;
}

let outputString = identity<string>("hello"); // Type is string
let outputNumber = identity<number>(42); // Type is number

In this example, T acts as a placeholder for the type of the argument and return value.

Using Type Inference

TypeScript can often infer the type parameter, so you don’t always need to specify it explicitly:

let output = identity("hello"); // Type is inferred as string

Working with Arrays and Generics

Generics shine when working with collections like arrays.

Example: Generic Array Function

Let’s create a function that logs the first element of an array:

function logFirstElement<T>(arr: T[]): void {
  console.log(arr[0]);
}

logFirstElement([1, 2, 3]); // Logs 1
logFirstElement(["a", "b", "c"]); // Logs "a"

Generic Array Helper Functions

You can also create reusable helper functions for arrays:

function getLastElement<T>(arr: T[]): T {
  return arr[arr.length - 1];
}

let lastString = getLastElement(["a", "b", "c"]); // "c"
let lastNumber = getLastElement([1, 2, 3]); // 3

Generics in Interfaces

Generics aren’t limited to functions; you can use them in interfaces too.

Example: Generic Box Interface

Imagine you want an interface that represents a box containing a value of any type:

interface Box<T> {
  value: T;
}

let stringBox: Box<string> = { value: "hello" };
let numberBox: Box<number> = { value: 42 };

This approach ensures that the value property maintains its type integrity.

Generic Classes

Generics are incredibly powerful in classes, allowing you to create reusable class components.

Example: Generic Stack Class

Let’s create a stack class that can handle any type:

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

  push(element: T): void {
    this.elements.push(element);
  }

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

  peek(): T | undefined {
    return this.elements[this.elements.length - 1];
  }
}

let stringStack = new Stack<string>();
stringStack.push("hello");
console.log(stringStack.peek()); // "hello"

let numberStack = new Stack<number>();
numberStack.push(42);
console.log(numberStack.peek()); // 42

Constraints: Making Generics More Specific

Sometimes, you want to constrain the types that can be used with a generic.

Example: Constrained Generic Function

Let’s create a function that only works with types that have a length property:

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

logLength("hello"); // Logs 5
logLength([1, 2, 3]); // Logs 3
// logLength(42); // Error: Number doesn't have a length property

Here, T extends { length: number } ensures that only types with a length property can be used.

Conclusion

Generics are a powerful feature in TypeScript that enable you to write flexible, reusable, and type-safe code. Whether you’re working with functions, interfaces, or classes, generics can help you reduce duplication and improve maintainability.

Happy coding!

TypeScript generics reusable code type safety generic functions generic classes

Related Articles

Generics in TypeScript: Write Flexible and Type-Safe Code

Generics in TypeScript: Write Flexible and Type-Safe Code

Learn TypeScript generics with easy, practical examples to write reusable and type-safe code. Perfect for beginners and pros alike.

TypeScript generics reusable code type safety generic functions
Load more articles