Nik FP
Nik FP

Reputation: 3093

Typescript higher order function preserve overload types

I'm trying to create a higher order function that can attach a primitive watcher to a method. It's working great as long as the input function is not overloaded, but I'm running into issues with overloads. I would like to find a way to pass all overloads through the higher order function so the returned function can behave just like the original. This is what I have so far:

I build a watcher with a function like this, giving me a simple way to track statistics of function use.

function makeWatcher() {
  let _callCount = 0;

  function countCall() {
    _callCount = _callCount + 1;
  }
  function getCallCount() {
    return _callCount;
  }

  return {
    countCall,
    getCallCount,
  };
}

I then have a higher order function that takes a function and a watcher as input, and returns a function that interacts with the watcher and then runs the original function.

// Generic type assures method signature is inferred. 
function watchMethod<T extends (...args: any[]) => any>(
  func: T,
  watcher: Watcher
): (...funcArgs: Parameters<T>) => ReturnType<T> {

  // Return a new function that has same signature as input function,
  // with added watcher interaction when called
  return (...args: Parameters<T>): ReturnType<T> => {

    // Interact with watcher
    watcher.countCall();

    // execute original function and return results
    const results = func(...args);
    return results;
  };
}

The last step is to compose these together to give me a method that has properties I can interact with, like so:

function buildWatchedMethod<T extends (...args: any[]) => any>(inputFunction: T) {
  
  // get a watcher
  const watcher = makeWatcher();

  // set up method watching
  const watchedMethod= watchMethod(inputFunction, watcher);

  // list out properties that should be public
  const externalProperties = {
    getCallCount: watcher.getCallCount
  }

  // assign properties to method
  return Object.assign(watchedMethod, externalProperties);
}

I can then add watchers to methods like this:

function method(input: string){
  console.log(input);
}

const watchedMethod = buildWatchedMethod(method);

watchedMethod("Hello World"); // logs "Hello World"
watchedMethod.getCallCount(); // returns 1 - method was called once
watchedMethod("Hello again"); // logs "Hello Again"
watchedMethod.getCallCount(); // returns 2
watchedMethod.countCall(); // error - not available from outside function

This works beautifully, function signatures are preserved, type safety is preserved - until buildWatchedMethod is called with an overloaded method. It still works, but the overloads of the watched methods aren't preserved through the process, which makes typescript complain.

This appears to be expected behavior as outlined in the accepted answer of this post, but is there any way to carry the overload signatures through the higher order function?

Where the issue comes up most often is when I use libraries where overloads are common. (I'm looking at you, Mongoose!) I'm also trying to avoid mocking libraries like Sinon because a) they can be painful to use with typescript sometimes, and b) my needs for this use case are very minimal, and third party libraries tend to have a lot more involved than what I need.

Any suggestions are appreciated!

Upvotes: 0

Views: 285

Answers (1)

Matt Diamond
Matt Diamond

Reputation: 11696

I believe there's a way to make this work.

The problem is the watchMethod return type... representing the original function type using Parameters<T> doesn't appear to capture the overloads (since Parameters only returns a single parameter list). However, if we return T directly, the overloads can make it through. The only catch is that Typescript will complain about the return type, so you're going to have to use a type assertion. If you're willing to do that, then the following modification should solve your problem:

function watchMethod<T extends (...args: any[]) => any>(
  func: T,
  watcher: Watcher
): T {

  // Return a new function that has same signature as input function,
  // with added watcher interaction when called
  return ((...args: Parameters<T>): ReturnType<T> => {

    // Interact with watcher
    watcher.countCall();

    // execute original function and return results
    const results = func(...args);
    return results;
  }) as T;
}

Upvotes: 1

Related Questions