Skip to Content
Course content

35: Utility Types: ReturnType and Parameters

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

I was working on a project recently where I had a few complex "service" functions that handled API calls. The problem is that these functions often evolve. I'd add a field to the response object or change a parameter, and then I'd spend half my morning hunting down every single place where I'd manually typed that response shape just to fix a type error.

The Maintenance Headache

Let's look at a typical scenario. Imagine we have a function that fetches a user's configuration. I've let TypeScript infer the return type because the object is a bit nested and annoying to write out manually:

function getUserConfig(userId: string) {
  return {
    id: userId,
    settings: {
      theme: 'dark',
      notifications: true,
      language: 'en-US'
    },
    lastLogin: new Date()
  };
}

Now, here's where it gets annoying. I want to write a "mock" version of this function for my tests. My first instinct is to just define a type for the return value:

type UserConfig = {
  id: string;
  settings: { theme: string; notifications: boolean; language: string };
  lastLogin: Date;
};

const mockGetUserConfig = (userId: string): UserConfig => {
  return {
    id: userId,
    settings: { theme: 'light', notifications: false, language: 'fr-FR' },
    lastLogin: new Date()
  };
};

This works fine today. But what happens if I add a timezone field to the real getUserConfig? I now have to remember to update the UserConfig type manually, or my mock will be out of sync with the real implementation. That's a recipe for bugs that only show up at runtime.

Extracting the Return Type

I wondered if there was a way to tell TypeScript: "Just look at getUserConfig and tell me what it returns." That's where ReturnType comes in. Now, if I try ReturnType<getUserConfig>, TypeScript screams at me. It wants a type, but getUserConfig is a value (the function itself).

To fix this, I have to use the typeof operator first to get the type signature of the function:

type ConfigReturn = ReturnType<typeof getUserConfig>;

// Now I can use ConfigReturn for my mock!
const mockGetUserConfig = (userId: string): ConfigReturn => {
  return {
    id: userId,
    settings: { theme: 'light', notifications: false, language: 'fr-FR' },
    lastLogin: new Date()
  };
};

This is much cleaner. If I add a field to the original function, ConfigReturn updates automatically. I'm no longer duplicating the truth.

Figuring Out the Inputs

But we can take this further. What if I want to create a higher-order function—something that wraps getUserConfig to add logging or timing? I don't want to manually define the arguments for the wrapper either, because if I change userId: string to userId: number, I don't want to hunt through my wrappers to fix the types.

I tried using ReturnType, but obviously, that's for the output. I need the inputs. That's where Parameters comes in. Let's see what happens when we use it:

type ConfigParams = Parameters<typeof getUserConfig>;
// ConfigParams is actually [userId: string]

Notice that Parameters returns a tuple. This makes sense because functions can have multiple arguments. If I have a function with three arguments, Parameters gives me an array-like type with three elements. This is incredibly useful when combined with the spread operator.

Putting it All Together

Let's build a generic withLogging wrapper. I want this to work for any function, regardless of what it takes or returns. I'll use generics combined with our two new utility types:

function withLogging<T extends (...args: any[]) => any>(fn: T) {
  return (...args: Parameters<T>): ReturnType<T> => {
    console.log(`Calling function with args:`, args);
    const result = fn(...args);
    console.log(`Result:`, result);
    return result;
  };
}

const loggedGetUserConfig = withLogging(getUserConfig);

// TypeScript knows exactly that:
// 1. It takes a string (from Parameters)
// 2. It returns the Config object (from ReturnType)
const config = loggedGetUserConfig('user-123');

I love this pattern because it's completely decoupled. I can pass any function into withLogging, and the resulting function maintains the exact type safety of the original. No any, no manual interface updates, just pure type extraction.




📋 Practical Task

Exercise: Implementing a Type-Safe Event Dispatcher Mock

You are building a testing utility for an event system. You have a core function called dispatchEvent that handles sending events to a server. You need to create a createMockDispatcher function that returns a mock version of the dispatcher, but it must perfectly mirror the types of the original.

Requirements:

  • Use the provided dispatchEvent function as the source of truth.
  • Use Parameters to ensure the mock function accepts the exact same arguments as the original.
  • Use ReturnType to ensure the mock function returns the exact same type as the original.
  • The mock function should simply console.log the arguments and return a dummy value that matches the expected return type.
// The source of truth
function dispatchEvent(eventName: string, payload: object, priority: number) {
  return {
    success: true,
    timestamp: Date.now(),
    eventId: Math.random().toString(36)
  };
}

// YOUR TASK: 
// Create a function called createMockDispatcher that returns 
// a function with the same Parameters and ReturnType as dispatchEvent.

function createMockDispatcher() {
  // Implement here
}

const mock = createMockDispatcher();
// This call should be type-checked against the original dispatchEvent signature
mock('user_signup', { email: 'test@test.com' }, 1);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.