Search code examples
typescripttyping

Infer type from object property


Is it possible to create a generic function that infers the type from the "external" object property?

Imagine something like this:

const resolversMap: {
  overallRankingPlacement: (issuer: number, args: Record<string, any>, context: Record<string, any>) => Promise<number>;
} = {
  overallRankingPlacement: createResolver((issuer, args, context) => {
    return 0;
  }),
};

function createResolver<T extends () => void>(resolverFn: T): ReturnType<T> {
  return (...args) => {
    // Some additional logic...
    resolverFn(...args);
  };
}

The goal would be so that the function returned by createResolver would match the exact return of "overallRankingPlacement" and the arguments passed to it would match just the same—kind of mirroring/proxying everything in regards to type.


Solution

  • You can do this by using generic parameters for both the argument-array and the result type. Something like this will work:

    function createResolver<Args extends unknown[], Res>(
      resolverFn: (...args: Args) => Res
    ): (...args: Args) => Res {
      return (...args: Args) => {
        // Some additional logic...
        return resolverFn(...args);
      };
    }
    

    The resolversMap example above will still fail though, as the overallRankingPlacement function returns a promise, whereas the argument to createResolver doesn't. You can fix it by removing Promise from the return type or passing an asynchronous function to createResolver to get the types in line.

    Maybe what you're looking for is a createResolver that returns an async function? If so, you could add a Promise and an async to the function above and get:

    function createAsyncResolver<Args extends unknown[], Res>(
      resolverFn: (...args: Args) => Res
    ): (...args: Args) => Promise<Res> {
      return async (...args: Args) => {
        // Some additional async logic...
        return resolverFn(...args);
      };
    }
    

    This version will work correctly with your sample resolversMap.

    It may be worth noting that wrapping functions like this does not handle overloaded functions well. Only the last overload is retained, so for

    function overloaded(x: number): number
    function overloaded(x: string): string
    function overloaded(x: number | string) {
        return x
    }
    

    the wrapped function will only be from string to string:

    const unfortunatelyNotOverloaded = createResolver(overloaded)
    const ok = unfortunatelyNotOverloaded('ok') // inferred type: string
    const fail = unfortunatelyNotOverloaded(1) 
    // ERROR: Argument of type 'number' is not assignable to parameter of type 'string'.
    

    I don't think there's any way around this yet, and maybe it won't even get supported at all.

    TypeScript playground