DEV Community

Justin Calleja
Justin Calleja

Posted on

Creating different Typescript function type signatures based on types bound to generics on use

Just wanted to make a note of this more than anything else. I spent some time the other day trying to figure out how I can make "one type" for all my API client functions. After a bit of struggling, I figured I can't really have them all extend something more generic as… well, they're different types of functions:

  • Some take no args
  • Some take only the resource id
  • Some take only the request body
  • Some take both the resource id and the request body

… and they all return the same type (of course) - which is not really important here.

Finally, I decided to look into conditional types in TS. I've known about this feature but hadn't actually used it in a "real" code-base. I still have to get my head around "naked" types and what-not, but basically, managed to get a good enough solution (for me) with this:

export type ApiClientFn<TId, TReqBody, TResBody> = [TId, TReqBody] extends [
  undefined,
  undefined,
]
  ? () => ApiClientRes<TResBody>
  : [TId, TReqBody] extends [number, undefined]
  ? (id: number) => ApiClientRes<TResBody>
  : [TId, TReqBody] extends [undefined, TReqBody]
  ? (reqBody: TReqBody) => ApiClientRes<TResBody>
  : [TId, TReqBody] extends [number, TReqBody]
  ? (id: number, reqBody: TReqBody) => ApiClientRes<TResBody>
  : never;
Enter fullscreen mode Exit fullscreen mode

Basically, I can now create the different variations of API client function types using ApiClientFn - passing it 3 arguments to bind the generic types:

const getOne: ApiClientFn<number, TReqBody, TResBody> = async (id) => { // …

const getList: ApiClientFn<undefined, TReqBody, TResBody> = async () => { // …

const create: ApiClientFn<undefined, TReqBody, TResBody> = async (reqBody) => { // …

export const update: ApiClientFn<number, TReqBody, TResBody> = async (
  id,
  requestBody,
) => { // …
Enter fullscreen mode Exit fullscreen mode

Wasn't what I initially had in mind. I initially set out to have one type to represent "any of the API client functions". Turns out I didn't actually need that type though. I had intended to use it in a useQueryApi function - in which I wanted to have one hook that takes any of my getter client API functions and uses it in a "common workflow" i.e. in order to DRY up concerns like throwing an Error on unsuccessful queries so react-query's useQuery works properly and redirecting to login page and logging out (and notification) etc… on specific status code responses.

Turns out, I didn't need a type for "all API client functions I have", because this code just cared about the return type really - so it ended up taking:

export type ApiFnWrapper<TResBody> = () => ApiClientRes<TResBody>;
Enter fullscreen mode Exit fullscreen mode

… and if the actual API client function takes any args, then they can always be closed over, like id is in () => getOne(id), below:

  export const useSomeResource = (id: number, queryKey: string = 'queryId') => {
  const { data, ...rest } = useQueryApi(() => getOne(id), {
    queryKey,
  });

  return {
    data: data?.data,
    ...rest,
  };
};
Enter fullscreen mode Exit fullscreen mode

Anyhow, the ApiClientFn "factory type function" - or whatever it's technical term is, allows me to:

  • see all type signatures of my API client functions in one place
  • not have to stay coming up with silly names for the different variations e.g. type APIClientFnThatTakesAnId = (id: number) => ApiClientRes<TResBody>

… and that seems like a win to me.

Top comments (0)