user3690467
user3690467

Reputation: 3387

TypeScript - generic being incorrectly inferred as unknown

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.

Upvotes: 11

Views: 6732

Answers (3)

ford04
ford04

Reputation: 74500

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

Upvotes: 9

dege
dege

Reputation: 2954

You did not pass the T to the function, so it does not know what it is. Check the following code:

const [data] = useClientRequest<Foo, typeof fooGetter>(fooGetter, 'url.com');

If you do it like this, it will know that data can be Foo | undefined.

Upvotes: -1

HTN
HTN

Reputation: 3604

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
  }

Upvotes: 5

Related Questions