Motivation

Very often in TypeScript, we would like to wrap an existing function into a wrapper function, with some additional preprocessing or postprocessing. For example,

function existingFunc(arg1: number, arg2: string): number {
  return arg1 + arg2.length;
}

function wrapperFunc(arg1: number, arg2: string): number {
  // Preprocess arg1 and arg2...
  return existingFunc(arg1, arg2);
}

In this case, the wrapper function often shares the same parameter types and return type. If the existing function is already typed, we can reuse those typing information. This would reduce redundancy and is also able to reflect any changes that would happen in the typing of the existing function in the future.

Solution 1: Use typeof

Defining the wrapper as an arrow function with a type of typeof existingFunc is the simplest solution:

const wrapperFunc: typeof existingFunc = (arg1, arg2) => {
  // Preprocess arg1 and arg2...
  return existingFunc(arg1, arg2);
};

Using the JavaScript spread syntax, we can further simplify the wrapper above to:

const wrapperFunc: typeof existingFunc = (...args) => {
  // Preprocess args...
  return existingFunc(...args);
};

Feel free to play with the example on TS PlayGround.

Solution 2: Use the Parameters and ReturnType Utility Types

The Parameters and ReturnType utility types can be used to retrieve the parameter types and the return type of the existing function, respectively. The above example becomes:

function wrapperFunc(
  arg1: Parameters<typeof existingFunc>[0],
  arg2: Parameters<typeof existingFunc>[1],
): ReturnType<typeof existingFunc> {
  // Preprocess arg1 and arg2...
  return existingFunc(arg1, arg2);
}

Using the JavaScript spread syntax, we can further simplify the wrapper above to:

function wrapperFunc(...args: Parameters<typeof existingFunc>): ReturnType<typeof existingFunc> {
  // Preprocess args...
  return existingFunc(...args);
}

It is also possible to add additional parameters to the wrapper function:

function wrapperFuncWithAdditionalParam(
  newArg: number,
  ...args: Parameters<typeof existingFunc>
): ReturnType<typeof existingFunc> {
  // Preprocess args...
  return existingFunc(...args);
}

Feel free to play with the example on TS PlayGround.

Compare the Two Solutions

Each solution has its advantages and disadvantages.

Solution 1Solution 2
SimplicitySimplestLess simple
Works if existing function is generic?YesNot in all cases
Easiness to modify signatureDifficultEasy
Declarable with function keyword?NoYes

On the one hand, Solution 1 has the advantage of being simple and also works across all cases in which the existing function is a generic function.

On the other hand, Solution 2 makes it easy to modify the signature of the wrapper function, such as adding additional parameters. Solution 2 also allows the wrapper function to be declared with the function keyword and thus has the corresponding advantages, e.g., clarity and that it can be used before where it is defined.

Real-World Example

spawnSync is a Node.js function that synchronously spawns a new process specified by the function parameters. If an error occurs, the returned object would contain an 'error' property.

However, in some situations, such as test code, we may simply prefer the function to throw an error in case an error occurs. We can define a new function that wraps spawnSync:

import { spawnSync } from "child_process";

// Same as spawnSync, except it throws an error if the spawn fails.
function spawnSyncWithError(...args: Parameters<typeof spawnSync>): ReturnType<typeof spawnSync> {
  const result = spawnSync(...args);
  if ("error" in result) {
    throw new Error(JSON.stringify(result));
  }
  return result;
}

(This real-world example was taken from typedoc-plugin-404.)

Wanna get the most out of TypeScript? Check out Essential TypeScript 5, Third Edition by Adam Freeman! (affiliate link)
Essential TypeScript Book Cover