Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript - generic being incorrectly inferred as unknown

Tags:

typescript

I have the following generic function:

export function useClientRequest<T, R extends (...args: any) => AxiosPromise<T>>(
  func: R,
  ...args: Parameters<R>
): [T | undefined, boolean, AxiosError | undefined] {
  // Irrelevant
}

Summarily, the function's return contains a value of type T which should be inferred as described above.

I then try to use it as follows:

interface Foo {
  // ...
}

function fooGetter(url: string): AxiosPromise<Foo> {
  return Axios.get<Foo>(url);
}

const [data] = useClientRequest(fooGetter, 'url.com');

However my IDE reports that data is of type unknown, because T is being inferred as unknown.

Am I doing something wrong or is this a TypeScript limitation?

Typescript v3.7.2

I know I can specify the type parameters. I'm wondering why they are being inferred incorrectly and if I can somehow change the implementation to help the inferring mechanism.

like image 616
user3690467 Avatar asked Nov 09 '19 12:11

user3690467


2 Answers

From TypeScript specifications:

Type parameters may be referenced in parameter types and return type annotations, but not in type parameter constraints, of the call signature in which they are introduced.

Given your function signature,

<T, R extends (...args: any) => AxiosPromise<T>>(
  func: R, ...args: Parameters<R>
): [T | undefined, boolean, AxiosError | undefined]

, my interpretation of above statement is, that T appears in the type parameter constraint signature extends (...args: any) => AxiosPromise<T> of parameter R and can therefore not be resolved properly. unknown is just the implicit default constraint type of generic type parameters.

So these contrived examples would work:

declare function fn0<T, U extends T>(fn: (t: T) => U): U
const fn0Res = fn0((arg: { a: string }) => ({ a: "foo", b: 42 }))  // {a: string; b: number;}

declare function fn1<T, F extends (args: string) => number>(fn: F, t: T): T
const fn1Res = fn1((a: string) => 33, 42) // 42

In the next two samples the compiler infers T to be unknown, because T is only referenced in the call signature constraint of U and not used in function parameter code locations for further compiler hints:

declare function fn2<T, U extends (args: T) => number>(fn: U): T
const fn2Res = fn2((arg: number) => 32) // T defaults to unknown

declare function fn3<T, U extends (...args: any) => T>(fn: U): T
const fn3Res = fn3((arg: number) => 42) // T defaults to unknown

Possible solutions (choose, what fits best)

1.) You can introduce type parameters T and R just for the function parameters and return type:

declare function useClientRequest2<T, R extends any[]>(
    func: (...args: R) => Promise<T>,
    ...args: R
): [T | undefined, boolean, AxiosError | undefined]

const [data] = useClientRequest2(fooGetter, 'url.com'); // data: Foo | undefined

2.) Here is an alternative with conditional types (a bit more verbose):

declare function useClientRequestAlt<R extends (...args: any) => Promise<any>>(
    func: R,
    ...args: Parameters<R>
): [ResolvedPromise<ReturnType<R>> | undefined, boolean, AxiosError | undefined]

type ResolvedPromise<T extends Promise<any>> = T extends Promise<infer R> ? R : never
const [data2] = useClientRequestAlt(fooGetter, 'url.com'); // const data2: Foo | undefined

Playground

like image 80
ford04 Avatar answered Nov 13 '22 14:11

ford04


I can't answer why it does not work. However, I would do something like this:

type UnpackedAxiosPromise<T> = T extends AxiosPromise<infer U> ? U : T; 

function useClientRequest<R extends (...args: any) => any>(
    func: R,
    ...args: Parameters<R>
  ): [UnpackedAxiosPromise<ReturnType<R>> | undefined, boolean, AxiosError | undefined] {
    //irrelevant
  }
like image 30
HTN Avatar answered Nov 13 '22 13:11

HTN