Home     /Articles     /

TypeScript Generics: Unlocking Reusable and Flexible Code

Typescript

TypeScript Generics: Unlocking Reusable and Flexible Code

Written by Briann     |

December 03, 2024     |

1.4k |

Generics in TypeScript allow you to write reusable, flexible code that works with a wide variety of types while maintaining type safety. By using generics, you can define components, functions, or classes that aren't tied to a specific data type, making your codebase more robust and adaptable.


In this post, we’ll explore the power of generics, understand their syntax, and apply them through practical examples.





What Are Generics?

Generics let you create type-safe abstractions. Instead of hardcoding a specific type, you can define a placeholder (often called T) that gets replaced with the actual type during usage.


Basic Syntax:

function identity(value: T): T {  return value;}

const number = identity(42); // T is inferred as number
const text = identity("Hello, World!"); // T is inferred as string

In the example above, the function identity works with any type, determined dynamically when it’s called





Generic Functions

Generics are commonly used in functions to enforce type safety while maintaining flexibility.


Example: A Generic Array Filter:

function filterArray(arr: T[], callback: (item: T) => boolean): T[] {
  return arr.filter(callback);
}

const numbers = [1, 2, 3, 4];
const evenNumbers = filterArray(numbers, (num) => num % 2 === 0);
console.log(evenNumbers); // [2, 4]

Here, the function works with arrays of any type (T[]), ensuring that the input, callback, and output remain consistent.





Generic Interfaces

Generics also shine in defining flexible interfaces.


Example: A Key-Value Store Interface:

interface KeyValueStore {
  key: K;
  value: V;
}

const userStore: KeyValueStore = {
  key: "userId",
  value: 101,
};

This pattern is especially useful for data structures like maps or dictionaries.





Generic Classes

When building reusable components, generic classes allow you to define data structures that handle a wide range of types.


Example: A Generic Stack:

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

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

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

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

const numberStack = new Stack();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.peek()); // 2
numberStack.pop();

const stringStack = new Stack();
stringStack.push("Hello");
console.log(stringStack.peek()); // "Hello"





Generic Constraints

Sometimes, you want to restrict the types that can be used with a generic. This is done using constraints like extends.


Example: Enforcing Object Properties:

function getProperty(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice" };
console.log(getProperty(user, "name")); // "Alice"

// Error: Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'
console.log(getProperty(user, "age"));

The keyof T constraint ensures that key must be a property of the object T.





Generic Default Types

You can provide default types for generics to simplify usage.


Example:

interface ApiResponse {
  data: T;
  status: number;
}

const response: ApiResponse = {
  data: { id: 1, name: "Alice" },
  status: 200,
};

Here, T defaults to any if no type is specified explicitly.





Using Generics with React Components

Generics are widely used in frameworks like React for type-safe components and hooks.


Example: A Generic Dropdown Component:

import React from "react";

interface DropdownProps<T> {
  options: T[];
  onSelect: (value: T) => void;
} 

function Dropdown<T>({ options, onSelect }: DropdownProps<T>) {
  return (
    <select onChange={(e) => onSelect(options[+e.target.value])}>
      {options.map((option, index) => (
        <option key={index} value={index}>
          {String(option)}
        option>
      ))}
    select>
  );
}

// Usage with strings
<Dropdown
  options={["Apple", "Banana", "Cherry"]}
  onSelect={(value) => console.log(value)}
/>;

// Usage with numbers
<Dropdown
  options={[1, 2, 3]}
  onSelect={(value) => console.log(value)}
/>;





Conclusion


Generics in TypeScript unlock powerful ways to write reusable and flexible code. They ensure type safety while allowing developers to define abstractions that adapt to varying use cases. Whether you’re working with functions, interfaces, or classes, mastering generics is an essential skill for any TypeScript developer.

Related Articles