How to type Axios.all when the each promise has a different return type

Viewed 3557

I have two Axios get requests, for which the return types have been explicitly declared. Here's a simplified example:

interface O1 {
   prop1: string;
   prop2: boolean;
}

interface O2 {
   prop3: number;
   prop4: string;
}

interface Request<T> {
   message: string;
   data: T
}

Using these interfaces, I've typed my requests and the result object is also typed:

const getO1 = () => Axios.get<Request<O1>>("/API/O1");
const getO2 = () => Axios.get<Request<O2>>("/API/O2");

Now I need to make this calls in parallel, so I've tried Axiox.all but TypeScript complains the the O1 and O2 types do not match. Specifically, I've tried:

const getAll = () => Axios.all([getO1(), getO2()]);

From what I can see, TypeScript expects that, in the case of all, all results will have the same type (structure). Is there a way of having a typed concurrent call? (using Axios v0.18 and TypeScript v3.0.3)

2 Answers

Unfortunately the declaration for Axios.all is not very good. The way it is typed you can't pass in a tuple.

export interface AxiosStatic extends AxiosInstance {
  all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
}

The parameter has to be an array of a single type T. Typescript will not infer a union for T and can't infer a tuple where an array is expected.

We can have a look at how Promise.all is typed (which does support tuples)

all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;
// ...
all<T1, T2, T3, T4>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>]): Promise<[T1, T2, T3, T4]>;
all<T1, T2, T3>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
all<T1, T2>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;
all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]>;

We can see that they use a lot of overloads to support tuples.

If Axios promises are spec compliant, you could use try using Promise.all. The types work out, but I have not tested the runtime behavior myself.

const getAll = () => Promise.all([getO1(), getO2()]) //() => Promise<[AxiosResponse<Request<O1>>, AxiosResponse<Request<O2>>]>

Or you can stick with the axios version and use some type assertions:

const getAll = () => Axios.all<{}>([getO1(), getO2()]) as unknown as AxiosPromise<[Request<O1>, Request<O2>]>

Just a fun note, in 3.1 we will be able to type all without the 10 overloads:

type PromiseResult<T> = T extends AxiosPromise<infer R>? R: T
type ArrayPromiseResult<T extends (any | AxiosPromise<any>)[]> = { [P in keyof T]: PromiseResult<T[P]> }
declare function all<T extends [] | (any | AxiosPromise<any>)[]>(values: T): AxiosPromise<ArrayPromiseResult<T>>

let result = all([getO1(),getO2()]) // result is AxiosPromise<[Request<O1>, Request<O2>]>

You can create an base interface and let your O1 and O2 interfaceses extending this interface. Use the base interface as the generic type paramter inside the Axios.all statement.

  import Axios from 'axios';

    //base interface
    interface axiosResponse{}

    //extend the base interface in both interfaces
    interface O1 extends axiosResponse {
        prop1: string;
        prop2: boolean;
    }

    interface O2 extends axiosResponse {
        prop3: number;
        prop4: string;
    }

    interface Request<T> {
        message: string;
        data: T
    }

    const getO1 = () => Axios.get<Request<O1>>("/API/O1");
    const getO2 = () => Axios.get<Request<O2>>("/API/O2");

    //specify the base interface type as the generic type parameter
    const getAll = () => Axios.all<axiosResponse>([getO1(), getO2()]).then( r => {
      const result: O1 = <O1>r[0];
      const result2: O2 = <O2>r[1];
    })
Related