0

I am struggling with typescript overload resolution.

I am using googleapis library with typescript and trying to fetch all tagmanager accounts records. Since googleapis list function requires pagination if response body contains nextPageToken, I would like to create function that paginates all and grabs all records.

My idea is to create a curry function like this. It takes list function as an argument, and it keeps calling the list function with nextPageToken until nextPageToken is not included in returned data from the list function.

// ex. 1)
const allAccounts = await paginateAll(tagmanager.accounts.list)({
  // specify tagmanager.accounts.list parameter
});

// ex. 2)
const allContainers = await paginateAll(tagmanager.accounts.containers.list)({
  // specify tagmanager.accounts.containers.list parameter
});

I created a paginateAll signature like below, but it seems like typescript doesn't resolve proper overload.

export const paginateAll = <P1, P2, R>(
  list: (params?: P1, options?: P2) => Promise<R>,
): ((arg1: P1, arg2: P2) => Promise<Array<R>>) => async (a, b) => {
    // some procedure...
  };

const fetchAllAccounts = paginateAll(tagmanager.accounts.list)
                                     ^^^

=== ERROR ===
Argument of type '{ (params: Params$Resource$Accounts$List, options: StreamMethodOptions): GaxiosPromise<Readable>; (params?: Params$Resource$Accounts$List | undefined, options?: MethodOptions | undefined): GaxiosPromise<...>; (params: Params$Resource$Accounts$List, options: StreamMethodOptions | BodyResponseCallback<...>, callback: ...' is not assignable to parameter of type '(params?: BodyResponseCallback<Schema$ListAccountsResponse> | undefined, options?: unknown) => Promise<unknown>'.
  Types of parameters 'params' and 'params' are incompatible.
    Type 'BodyResponseCallback<Schema$ListAccountsResponse> | undefined' is not assignable to type 'Params$Resource$Accounts$List'.
      Type 'undefined' is not assignable to type 'Params$Resource$Accounts$List'.

googleapis list function has 6 overloads like below. I expect paginateAll to pick the 2nd signature.

1. list(params: Params$Resource$Accounts$List, options: StreamMethodOptions): GaxiosPromise<Readable>;
2. list(params?: Params$Resource$Accounts$List, options?: MethodOptions): GaxiosPromise<Schema$ListAccountsResponse>;
3. list(params: Params$Resource$Accounts$List, options: StreamMethodOptions | BodyResponseCallback<Readable>, callback: BodyResponseCallback<Readable>): void;
4. list(params: Params$Resource$Accounts$List, options: MethodOptions | BodyResponseCallback<Schema$ListAccountsResponse>, callback: BodyResponseCallback<Schema$ListAccountsResponse>): void;
5. list(params: Params$Resource$Accounts$List, callback: BodyResponseCallback<Schema$ListAccountsResponse>): void;
6. list(callback: BodyResponseCallback<Schema$ListAccountsResponse>): void;

I would really appreciate if you guys tell me why this is happening...

==== UPDATE ====

I reproduced the error in my question with TypeScript Playground. (I made it not curry to make it simpler)

type Params = {
    value1: string;
    value2: string;
}

type Options1 = {
    option1Value: string;
};

type Options2 = {
    option2Value: string;
};

type Resonse1 = {
    response1Value: string;
}

type Response2 = {
    response2Value: string;
}

type Callback<T> = () => T

declare function func(params: Params, options: Options1): Promise<Resonse1>;
declare function func(params?: Params, options?: Options2): Promise<Response2>;
declare function func(params: Params, options: Options1 | Callback<Resonse1>, callback: Callback<Resonse1>): void;
declare function func(params: Params, options: Options2 | Callback<Response2>, callback: Callback<Response2>): void;
declare function func(params: Params, callback: Callback<Response2>): void;
declare function func(callback: Callback<Response2>): void;

const anotherFunc = async <P1, P2, R>(
  fn: (params?: P1, options?: P2) => Promise<R>,
): Promise<R> => {
    return fn();
}

const test = anotherFunc(func);
2
  • 1
    It's happening because overload resolution only happens if you call the overloaded function directly. It does not happen via generic inference, and ends up picking the first or last overload. See this comment and the GitHub issue it's part of. Can you make the code in your question a minimal reproducible example that can be dropped as-is into a standalone IDE like The TypeScript Plaground to demonstrate your issue? This will allow me to write a version of it which will work for you. Commented Oct 8, 2020 at 15:10
  • @jcalz Thank you for your comment! I pasted more reproducible example in my question. (I don't know if it's really minimal) I would really really appreciate if you show me the work arounds for this issue... Thank you. Commented Oct 8, 2020 at 15:36

1 Answer 1

1

The compiler is not able to select an overload unless you actually call the overloaded function directly with inputs. If you pass the overloaded function to another function and try to use generic type inference, the compiler will not perform overload resolution the way you want. It just picks an overload, usually the first or the last one. This is a design limitation of TypeScript, see microsoft/TypeScript#30369 for more info.

The workaround here is for you to pick the signature you want to use. Here's one way:

const f: (params?: Params, options?: Options2) => Promise<Response2> = func;
const test = anotherFunc(f);

Here we assign func to a variable f of the type corresponding to a function which only has the call signature we wanted to infer. This assignment is allowed. Since f is not overloaded, the call anotherFunc(f) works as desired.

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

It looks like it's not possible to declare the type explicitly in my case, so I have to think other way around. But anyways, thanks for your answer. You answer will be my first step to learn more about typescript typings!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.