Back to Blog
Lesson 20 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 7, 20264 min read

Introduction to Generics: Writing Reusable TypeScript Code

Learn how to use Generics in TypeScript to create flexible, reusable functions that maintain strict type safety across your entire application.

TypeScriptGenericsReusabilityFunctionsTypes
Vibrant JavaScript code displayed on a screen, highlighting programming concepts and software development.

Previously in this course, we explored Mastering Discriminated Unions for Type-Safe API Responses to handle complex API states. While unions allow us to model specific outcomes, we often find ourselves writing the same logic repeatedly just to handle slightly different data shapes. This lesson introduces Generics, the ultimate tool for achieving code reusability without sacrificing the strict type safety you’ve come to rely on.

The Problem: Why Generics Matter

Imagine you need a function that returns the first element of an array. If you only deal with numbers, you might write:

TYPESCRIPT
function getFirstNumber(items: number[]): number {
  return items[0];
}

But what if you suddenly need to do the same for strings, or for your Task interface? You would end up duplicating this logic, leading to "code bloat." While you could use any to bypass the issue, you lose all the benefits of TypeScript’s validation, as Defining Function Return Types in TypeScript: A Practical Guide emphasizes the importance of predictable, explicit types.

Generics solve this by allowing you to define a "type placeholder" that is filled in when the function is actually called.

Creating Your First Generic Function

Think of a generic as a parameter for types. Instead of hardcoding number or string, we define a variable—usually named T (for Type)—that represents whatever type the user passes in.

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

// Usage
const firstTask = getFirstElement<Task>(allTasks);
const firstNumber = getFirstElement<number>([1, 2, 3]);

In the code above:

  1. <T> declares the generic type parameter.
  2. items: T[] tells TypeScript the function expects an array of whatever T is.
  3. : T tells TypeScript the return value will match that same type.

By passing <Task> or <number> at the call site, you are essentially "configuring" the function to work specifically for that data structure while keeping the logic shared.

Advancing the Task API Client

In our running project, we often receive different types of data from the server. Instead of writing a custom function for every response shape, we can use a generic wrapper. Let's create a identity-style helper that logs a response and returns it:

TYPESCRIPT
function logAndReturn<T>(data: T): T {
  console.log("Data received:", data);
  return data;
}

// Usage in our API client
const task = logAndReturn<Task>(apiResponse);

This ensures that task is correctly typed as Task without us having to write separate logging functions for Task, User, or Category interfaces.

Hands-on Exercise

Create a generic function called wrapInArray that takes a single argument of type T and returns an array containing that element.

  1. Define the function using the <T> syntax.
  2. Call the function with a string.
  3. Call the function with a Task object.
  4. Verify that the output types are string[] and Task[] respectively.

Common Pitfalls

  • Over-using Generics: Don't use them if a simple union or a specific type is sufficient. If a function only ever handles string or number, don't make it generic; just use a union type.
  • Cryptic Naming: While T is the industry standard for the first generic, use descriptive names like TResponse or TData if your function has multiple generic parameters.
  • Ignoring Constraints: Sometimes a generic is too flexible. If you try to access .id on a generic T, TypeScript will complain because it doesn't know if T has an id. We will cover how to solve this in the next lesson on constraining generics.

FAQ

Q: Are generics just a fancy name for any? A: Absolutely not. any tells TypeScript to stop checking types entirely. Generics preserve type information, allowing the compiler to verify that what you put in is what you get out.

Q: Can I use more than one generic? A: Yes! You can define multiple parameters like <T, U> to map inputs to different outputs.

Q: Do generics add runtime overhead? A: No. Generics are a compile-time construct. They are erased during the transpilation process, meaning they have zero impact on your application's bundle size or performance.

Recap

Generics allow you to write reusable functions that maintain strict types. By using a placeholder like <T>, you create flexible logic that adapts to any data structure while ensuring the compiler continues to protect your code from runtime errors.

Up next: Generics with Interfaces — where we apply these concepts to our API models for even greater consistency.

Similar Posts