1

I'm trying to implement a generic CRUD interface for my API in Typescript using Axios.

Suppose the API exposes endpoints for doing CRUD operations with 2 entities. Say the endpoints are /users and /posts/ and both take URL parameters offset and count (for pagination). Example of responses:

GET /users?offset=2&count=2

{
  "users": ["user1", "user2"],
  "total": 4 // the total number of users that exist
}

and similarly for posts instead the key for the array of posts is `"posts".

I tried to create a generic function to get either of users/posts as follows:

export const getPage =
  async <T extends Persistable>(offset: number, count: number): Promise<PaginatedResponse<T>> => {
    const response = await axios.get<PaginatedResponse<T>>(
      `${SOME_URL}/<BLANK>`,
      {
        params: { page, size },
        transformResponse: [
          (data) => ({
            ...data,
            items: data[<BLANK>],
          })
        ]
      },
    );
    return response.data;
  };

Where Persistable interface that both User and Post interfaces inherit from and PaginatedResponse interface looks as follows:

export interface PaginatedResponse<T> {
  readonly total: number;
  readonly items: T[];
}

Basically, I'd need to fill in the <BLANK>s by e.g. somehow getting strings "users"/"posts" respectively based on the passed type T.

Can anyone think of a way to achieve this, please?

1
  • The type system in TypeScript is compile-time only, there's no representation of it at run-time that you can assert on to determine this. You would need to fall back to a standard JavaScript way of doing it which might be, if you're using constructor functions for example, to use the name property of the constructor. Other options might be just setting a property yourself with the value or doing some structural comparison of the object to determine its type. Commented Jul 24, 2019 at 11:13

1 Answer 1

1

I may suggest following solution using function overloading. I don't recomment using generics in this situation as T is limited to either user or post. As TypeScript interface is about shape, with generics T can be virtually anything that extends Persistable

I have imagined that users and posts will not be array of strings, but array of objects. type property will create discriminated union and will stay after compilation.

I've added additional argument reqType to getPage function to distinguish between users and posts request. reqType may be either 'user' or 'post'. Other values will lead to error.

This User['type'] is indexed access operator, so if you'll add another type of response in future, reqType will be populated with type value automatically.

type User = { type: 'user', user: string };
type Post = { type: 'post', post: string };

export interface PaginatedResponse<T> {
    readonly total: number;
    readonly items: T[];
}

// This is type guard which will stay in run time
function reqTypeIsUser(a: User['type'] | Post['type']): a is User['type'] {
    return (a as User['type']) === 'user'
}

async function getPage
    (offset: number, count: number, reqType: User['type']): Promise<PaginatedResponse<User>>
async function getPage
    (offset: number, count: number, reqType: Post['type']): Promise<PaginatedResponse<Post>>
async function getPage
    (offset: number, count: number, reqType: User['type'] | Post['type']): Promise<PaginatedResponse<User | Post>> {

        const response = await axios.get<PaginatedResponse<User | Post>>(
            'url' + reqTypeIsUser(reqType) ? 'users' : 'posts',
            {
                params: { offset, count },
                transformResponse: [
                    (data) => ({
                        ...data,
                        items: data[reqType].map((d: string) => ({ type: reqType, [reqType]: d })),
                      })
                    ]
                  },
                );
                return response.data;
};

let user = getPage(1, 2, "user");  // user will be Promise<PaginatedResponse<User>>
Sign up to request clarification or add additional context in comments.

Comments

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.