typescriptaxios

Conditional type for Axios return based on function arguments


The function axiosGet:

export const axiosGet = async <TData>({
    url,
    params = {},
    onDone,
    onError
}: TypeAxiosArgs<TData>) => {
    
    try {
        const r = await axios.get<TypeAxiosData<TData, typeof params.paginate>>(url, { params })
        let ret
        if (params?.paginate) {
            ret = r.data
        } else {
            ret = r.data.data // <--- causing typescript to scream, check below
        }
        onDone?.(ret)
        return ret
    } catch (e) {
        console.error(e)
        onError?.(e)
    }
}

The types:

export type TypeAxiosArgs<TData> = {
    url: string
    params?: { paginate?: number } & Record<string, any>
    onDone?: (response: TypeAxiosData<TData, undefined>) => void
    onError?: (error: any) => void
}

export type TypeAxiosData<
    TData,
    TPaginate extends number | undefined
> = TPaginate extends undefined
    ? TData
    : {
            data: TData
            meta: {
                from: number
                to: number
                total: number
                last_page: number
            }
      }

I'm getting an error on r.data.data if params.paginate is undefined

Property 'data' does not exist on type 'TypeAxiosData<TData, number | undefined>'.
  Property 'data' does not exist on type 'TData'.ts(2339)

I feel like the problem is with the axios.get generic which needs to be something different but I can't figure out what.

Edit: Fixed TypeAxiosData and tried manual casting.

export const axiosGet = async <TData>({
    url,
    params = {},
    onDone,
    onError
}: TypeAxiosArgs<TData>) => {
    
    try {
        const r = await axios.get<TypeAxiosData<TData, typeof params.paginate>>(url, { params })
        let ret
        if (params?.paginate) {
            ret = r.data as TypeAxiosData<TData, number>
        } else {
            ret = r.data.data as TypeAxiosData<TData, undefined>
        }
        onDone?.(ret)
        return ret
    } catch (e) {
        console.error(e)
        onError?.(e)
    }
}
axiosGet<{ id: number, name: string }>({ url: "", params: { paginate: undefined } })
    .then((result) => {})

The problem I'm having is that the type of result is is getting declared as:

(parameter) result: {
    id: number;
    name: string;
} | {
    data: {
        id: number;
        name: string;
    };
    meta: {
        from: number;
        to: number;
        total: number;
        last_page: number;
    };
} | undefined

Instead of:

(parameter) result: {
    id: number;
    name: string;
} | undefined

Solution

  • First of all, you're trying to correlate your response type based on your params type, but there's nothing in your or axios's code which makes them at all related. Params are just an independent dictionary of properties and aren't related to the returned response type at all, so any checks on params (or params?.paginate in your example) won't make Typescript narrow down the response type.

    In fact, the relationship between params and response type seems to be a contract of your own, meaning that you're making assumptions about the API that you're consuming - you can obviously have an endpoint in the wild which completely ignores params.paginate and returns the data in an arbitrary format, at which point your code will simply break, even if you managed to get around the compilation errors. The real question is whether you'd actually like to keep this contract for ALL of your endpoints which you'll be calling via your own axiosGet wrapper.

    I would suggest to review the API design first and perhaps make paginated and non-paginated response structure more similar. For example, you could keep this format:

    export type TypeAxiosData<TData> = {
        data: TData
        meta?: {
            from: number
            to: number
            total: number
            last_page: number
        }
    }
    

    This way you can still expect some common format from all of your endpoints (which is much easier to implement via some global middleware), but your paginated and non-paginated responses are much more similar.

    Finally, I wouldn't rely on the pagination metadata to be present in the response, even if you asked for it in the params - the only way to make sure if it's actually present is to check the response payload. And believe me, it's much cheaper to check for the metadata presence in runtime than to assume it's always there on paginated endpoints, only to find out that you forgot to add it to one of those endpoints and now the things are crashing silently. This is why in the code example above I skipped the pagination conditional.

    If you REALLY want to make your response type dependent on the params, then you need to pass the params as the generic type to the response and make params generic in TypeAxiosArgs too.