Tom Esterez
Tom Esterez

Reputation: 22127

Call a generic function with matching generic arguments

I'm trying to write a function that takes as arguments another function and it's args, then executes the function with the provided args and return the result while preserving the types.

As long as the provided function has a static return type, it's fine. But when that function's return type depends on the params, I'm not able preserve the return type.

Here is my failed attempt. I hope it will clarify what I'm trying to achieve:

function f<A>(args: A) {
  return args
}
// Calling the fuction directely works as expected:
const result = f('test') // OK: type of result is 'test'

// I tried this to be more explicit about the return type but it does not work
type ReturnTypeWithArgs<
  F extends (args: any) => any,
  A extends Parameters<F>[0],
> = F extends (args: A) => infer R ? R : never

// This is the function I'm trying to build
function callFunction<F extends (args: any) => any, A extends Parameters<F>[0]>(
  f: F,
  args: A,
): ReturnTypeWithArgs<F, A> {
  return f(args)
}

// I'd like to get the same returned type when using callFunction. But it doesn't work:
const result2 = callFunction(f, 'test') // KO: typeof test2 is unknown

Upvotes: 2

Views: 2308

Answers (1)

jcalz
jcalz

Reputation: 327624

There's currently no purely type-level way to take a generic function type like <T>(t: A<T>) => R<T> and an argument type like X and get the compiler to tell you what the return type would be if you called the function with an argument of type X. You can't use conditional type inference like the ReturnType<T> or the Parameters<T> utility types; it ends up just replacing the generic type parameters with their (possibly implicit) constraints, so <T>(t: A<T>) => R<T> becomes just (t: A<unknown>) => R<unknown>).

There's an open suggestion at microsoft/TypeScript#40179 for type operators that answer the above question. It's marked as "Awaiting More Feedback", without much engagement. If you want to see it happen you might go there, give it a 👍, and describe your use case and why it's needed... but it probably wouldn't have much effect one way or the other. For now it's not part of the language.


Currently TypeScript's support for higher order generic manipulation all involves some value-level code that makes it into emitted JavaScript. If you call a generic function, it will perform the type manipulation you want, but you can't do it without a function somewhere.

Luckily you can write your callFunction() function, though, by refactoring to use separate generic type parameters for the arguments and return types. That is, don't start with F of some function type and try to tease apart its argument type A and return type R. Start with A and R directly. This will allow the compiler to perform its higher order type inference from generic functions as implemented in microsoft/TypeScript#30215:

function callFunction<A, R>(f: (arg: A) => R, args: A): R {
    return f(args)
}
const result = callFunction(f, 'test') // string

That's what you wanted to see. You don't get unknown, but the actual string type of the input.


Well, there's the small discrepancy of A and R being inferred as string instead of the literal type "test". If that matters you need to give the compiler a hint to infer narrower types there. There's an issue at microsoft/TypeScript#30680 asking for some easy way to do this, but for now I use the workaround of defining a Narrowable hint type as a constraint. It could look like:

type Narrowable = string | number | boolean | null | undefined | bigint | {};
function callFunction<A extends Narrowable, R>(
    f: (arg: A) => R,
    args: A,
): R {
    return f(args)
}
const result2 = callFunction(f, 'test') // "test"

Playground link to code

Upvotes: 3

Related Questions