Generic recursive type to extend a nested interface dynamically

279 Views Asked by At

I have an object that contains the methods for all of the endpoints of my API. The nested structure of the object mimics the URL endpoints. For example (in reality it's a slightly more deeply nested object):

/api/users

/api/recipes

/api/recipes/user

const api = {
    users: {
        get: await getUsers(),
        post: await postUser()
    },
    recipes: {
        get: await getRecipes(),
        post: await postRecipe(),
        user: {
            get: await getUserRecipes()
        }
    }
}

I'm writing a Next JS app. I have two instances of the object shown above. One is for the back end, where each method makes a call to the database. The other is for the front end where each method makes a HTTP request to the back end.

I am trying to keep my typings simple and not repeat myself across the codebase. In both instances of the object, the data returned is the same type for each method. That's obviously so, as the api.recipes.get method on the front end makes a http request that goes to a handler that then calls api.recipes.get on the back end.

So, I have an interface that describes each method - the params they receive and the data they return. Now, what I want to do is be able to use that interface to describe both instances of the api object, without having to write it out twice. Why? - because in reality the back end will return a API repsonse with a hasErrors property and either some errors or the data, whereas the front end instance will return a http response with the data in the data property.

I'm hoping I can write a recursive type that will take the response type as a generic parameter and extend the API interface, changing the return type of each method from the base data type, to the given return type, with the base data as a property of it.

I've had a go at writing such a type:

export type Unpromisify<T> = T extends PromiseLike<infer U> ? Unpromisify<U> : T;

type Error = {}; // some error

type HTTPResponse<T> = {
    status: number;
    data: T | Error[];
}

type APIResponse<T> = {
    hasErrors: true;
    errors: Error[];
} | {
    hasErrors: false;
    body: T;
}

type HTTPMethod<T extends ((...args: any) => Promise<any>)> = (params: Parameters<T>[0]) => Promise<HTTPResponse<Unpromisify<ReturnType<T>>>>;

type APIMethod<T extends ((...args: any) => Promise<any>)> = (params: Parameters<T>[0]) => Promise<APIResponse<Unpromisify<ReturnType<T>>>>;

// EDIT - added missing types, thanks to jcalz, to make example reproducible

interface UserParams { p: 1 }
interface User { u: 2 }
interface Recipe { r: 3 }
interface RecipeParams { p: 4 }
interface UserID { u: 5 }

interface API {
    users: {
        get: (params: UserParams) => Promise<User[]>;
        post: (body: User) => Promise<User>;
    },
    recipes: {
        get: (params: RecipeParams) => Promise<Recipe[]>;
        post: (body: Recipe) => Promise<Recipe>;
        user: {
            get: (params: UserID) => Promise<Recipe[]>;
        }
    }
}

type APIExtension<T extends ((...args: any) => Promise<any>) | {}, P extends 'API' | 'HTTP'> = 
T extends ((...args: any) => any) ? 
P extends 'API' ? APIMethod<T> : HTTPMethod<T> :
APIExtension<T,P>;

type BackEndAPI = APIExtension<API, 'API'>;

I feel like I'm going in the right direction, but unfortunately on the last line I'm getting the error:

    Type instantiation is excessively deep and possibly infinite.ts(2589)

I understand this error is thrown if the type recursion exceeds the set threshold, but I can't work out why that's happening.

0

There are 0 best solutions below