Skip to Content
Course content

219: Testing Generic Functions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I'll be honest: when I first started with TypeScript, I thought testing generic functions was a bit of a waste of time. My logic was, "If the type system says it works for T, it works for everything, right?" Wrong. TypeScript's type safety disappears the moment your code is transpiled to JavaScript. If your generic logic has a bug—like assuming a value is always an object when it could be a string—the compiler won't always save you from a runtime crash.

To show you what I mean, let's build a small utility that wraps raw API data into a standardized Response object. This is a pattern I use in almost every project to keep error handling consistent.

Creating a generic API wrapper

First, we need a type that can wrap any data payload, and a function to perform the wrapping. I want this to be flexible enough to handle a single user, a list of products, or even just a simple success message.

type ApiResponse<T> = {
  data: T;
  timestamp: number;
  status: 'success' | 'error';
};

function wrapResponse<T>(payload: T): ApiResponse<T> {
  return {
    data: payload,
    timestamp: Date.now(),
    status: 'success',
  };
}

The logic here is trivial, but the contract is what matters. We're promising that whatever goes in as T comes out as the data property of our response.

The first pass: testing with a simple string

When testing generics, I usually start with the simplest possible type—usually a string or a number. It strips away the noise of object properties and lets me verify the basic wrapper logic.

test('wraps a string response correctly', () => {
  const input = 'Operation completed';
  const result = wrapResponse(input);

  expect(result.data).toBe('Operation completed');
  expect(result.status).toBe('success');
  expect(typeof result.timestamp).toBe('number');
});

Wait, my test helper is losing type safety

Now, I don't like repeating myself. I decided to write a helper function to run these tests across different types. This is where I messed up. I wrote the helper like this:

// MISTAKE: Using 'any' inside the helper
function verifyWrapper(input: any) {
  const result = wrapResponse(input);
  expect(result.data).toEqual(input);
}

// I thought this was clever, but now I've lost all my TS benefits
verifyWrapper('Hello'); 
verifyWrapper({ id: 1 });

The problem? By using any in my helper, I've basically turned off TypeScript for my tests. If I accidentally changed wrapResponse to return a string instead of T, the helper wouldn't complain, and I'd only find out when the test actually failed at runtime. That defeats the purpose of using TypeScript for the tests in the first place.

I fixed this by making the test helper itself generic. It feels a bit more verbose, but it ensures that the input type and the resulting data type stay linked.

function verifyWrapper<T>(input: T) {
  const result = wrapResponse(input);
  // Now TS knows that result.data must be of type T
  expect(result.data).toEqual(input);
}

Verifying complex shapes

The real test for a generic function is whether it handles complex objects without mutating them or losing properties. I'll add a test case for a User object to make sure the generics are preserving the structure perfectly.

interface User {
  id: string;
  email: string;
  roles: string[];
}

test('preserves complex User object structure', () => {
  const mockUser: User = {
    id: 'u123',
    email: 'dev@example.com',
    roles: ['admin', 'editor']
  };

  const result = wrapResponse(mockUser);
  
  // We can now access roles safely because of the generic T
  expect(result.data.roles).toContain('admin');
  expect(result.data.id).toBe('u123');
});

By testing both a primitive (string) and a complex object (User), we've confirmed that the generic T is behaving predictably regardless of the data shape. That's the goal: confidence that your abstraction doesn't break when the data gets messy.




📋 Practical Task

Build a Generic Pagination Validator

You are tasked with creating a pagination wrapper and a corresponding test suite. Follow these requirements:

  • Create a generic type PaginatedResponse<T> that contains:
    • items: T[]
    • totalCount: number
    • currentPage: number
  • Write a function paginate<T>(items: T[], page: number, pageSize: number) that returns a PaginatedResponse<T>. (For the sake of this exercise, the function can just return the full list of items and the requested page number).
  • Write a test suite that:
    1. Tests the function with an array of numbers.
    2. Tests the function with an array of objects (e.g., { productName: string, price: number }).
    3. Includes a generic test helper function that accepts an array of type T and verifies that the items property of the result matches the input array.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.