Typescript: Using `ReturnType` with generic function parameters and generic property access

Created on 3 Sep 2018  ·  2Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 3.1.0-dev.20180901

Search Terms: ReturnType, generic, invoke, invoker, property

Code

function invoker <K extends string | number | symbol, A extends any[]> (key: K, ...args: A) {
  return <T extends Record<K, (...args: A) => any>> (obj: T): ReturnType<T[K]> => obj[key](...args)
}

const result = invoker('test', true)({ test: (a: boolean) => 123 })

Workaround (duplicate similar ReturnType-like type locally):

export type InvokeResult <T extends (...args: A) => any, A extends any[]> = T extends (...args: A) => infer R ? R : never

export function invoker <K extends string | number | symbol, A extends any[]> (key: K, ...args: A) {
  return <T extends Record<K, (...args: A) => any>> (obj: T): InvokeResult<T[K], A> => obj[key](...args)
}

Expected behavior: Valid program and use of ReturnType.

Actual behavior:

Type '(...args: A) => any' does not satisfy the constraint '(...args: any[]) => any'.
  Types of parameters 'args' and 'args' are incompatible.
    Type 'any[]' is not assignable to type 'A'.

Playground Link: https://www.typescriptlang.org/play/index.html#src=function%20invoker%20%3CK%20extends%20string%20%7C%20number%20%7C%20symbol%2C%20A%20extends%20any%5B%5D%3E%20(key%3A%20K%2C%20...args%3A%20A)%20%7B%0D%0A%20%20return%20%3CT%20extends%20Record%3CK%2C%20(...args%3A%20A)%20%3D%3E%20any%3E%3E%20(obj%3A%20T)%3A%20ReturnType%3CT%5BK%5D%3E%20%3D%3E%20obj%5Bkey%5D(...args)%0D%0A%7D

Related Issues: No, this is out of my skillset to debug tonight. Logging for posterity and help 😄 I did try different workarounds and found I couldn't workaround it with any usage of property access so I added that to the title. For instance, the exact same code inlined results in an empty interface:

export function invoker <K extends string | number | symbol, A extends any[]> (key: K, ...args: A) {
  return <T extends Record<K, (...args: A) => any>> (obj: T): T[K] extends (...args: A) => infer R ? R : never => obj[key](...args)
}

const result = invoker('test', true)({ test: (abc: boolean) => 123 })

More minimal repro which makes me realise it may not be an issue but maybe an enhance to ReturnType to specify the generic type parameters:

type Test <T extends (...args: A) => any, A extends any[]> = ReturnType<T>
Bug Fixed

Most helpful comment

Less safety version of ReturnType but working for all inputs:

type UnsafeReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

Update1
But inference not working properly for all cases.
Cases above working correct.
But not working for union type rest parameters case (with enabled strictFunctionTypes):

const foo = (...args: [string, number] | [1]) => ({
  type: 'type',
  args,
});

type t = UnsafeReturnType <typeof foo>; // any

All 2 comments

Yeah, that error is unfortunate. Here's a minimal repro:

type Foo<A extends any[]> = ReturnType<(...args: A) => string>;

In --strictFunctionTypes mode this produces an error similar to the one you're seeing, the issue being that (...args: A) => string does not satisfy the constraint (...args: any[]) => any because any[] is not assignable to A. It seems that we need to be a little more permissive in the way we check assignability of rest parameters with type any[].

Less safety version of ReturnType but working for all inputs:

type UnsafeReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

Update1
But inference not working properly for all cases.
Cases above working correct.
But not working for union type rest parameters case (with enabled strictFunctionTypes):

const foo = (...args: [string, number] | [1]) => ({
  type: 'type',
  args,
});

type t = UnsafeReturnType <typeof foo>; // any
Was this page helpful?
0 / 5 - 0 ratings