2

Sorry for the confusing title! I'll try to be as clear as possible here. Given the following interface (generated by openapi-typescript as an API definition):

TypeScript playground to see this in action

export interface paths {
  '/v1/some-path/:id': {
    get: {
      parameters: {
        query: {
          id: number;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
    post: {
      parameters: {
        body: {
          name: string;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
  };
}

The above interface paths will have many paths identified by a string, each having some methods available which then define the parameters and response type.

I am trying to write a generic apiCall function, such that given a path and a method knows the types of the parameters required, and the return type.

This is what I have so far:

type Path = keyof paths;
type PathMethods<P extends Path> = keyof paths[P];

type RequestParams<P extends Path, M extends PathMethods<P>> =
  paths[P][M]['parameters'];

type ResponseType<P extends Path, M extends PathMethods<P>> =
  paths[P][M]['responses'][200]['schema'];

export const apiCall = (
  path: Path,
  method: PathMethods<typeof path>,
  params: RequestParams<typeof path, typeof method>
): Promise<ResponseType<typeof path, typeof method>> => {
  const url = path;
  console.log('params', params);

  // method & url are 
  return fetch(url, { method }) as any;
};

However this won't work properly and I get the following errors:

  1. paths[P][M]['parameters']['path'] -> Type '"parameters"' cannot be used to index type 'paths[P][M]' Even though it does work (If I do type test = RequestParams<'/v1/some-path/:id', 'get'> then test shows the correct type)

Any idea how to achieve this?

2
  • Could you make sure the code here is a minimal reproducible example? I have a few issues; one is 'path' does truly seem like it should be an error here, and I can't reproduce your "method becomes type never for some reason". Presumably you want apiCall to be a generic function, but I can't see what your problem is for myself. Also, you might want to split this into multiple questions; your errors 1 and 2 seem related to each other, but 3 is just a different problem you are having with your code. Commented Sep 8, 2021 at 21:54
  • I could reduce it a bit but it might remove the main problem -> mainly being that path and method are necessary to determine the response type, while all 3 of them are "generic" in the sense that they are determined by the path. Problems 1 and 2 are the same, problem 3 might not be a problem -> see the TypeScript playground I added Commented Sep 9, 2021 at 10:55

2 Answers 2

3
+100

Solution

After few trials, this is the solution I found.

First, I used a conditional type to define RequestParams:

type RequestParams<P extends Path, M extends PathMethods<P>> = 
    "parameters" extends keyof paths[P][M] 
        ? paths[P][M]["parameters"]
        : undefined;

Because typescript deduces the type of path on the fly, the key parameters may not exist so we cant use it. The conditional type checks this specific case.

The same can be done for ResponseType (which will be more verbose) to access the properties typescript is complaining about.

Then, I updated the signature of the function apiCall:

export const apiCall = <P extends Path, M extends PathMethods<P>>(
  path: P,
  method: M,
  params: RequestParams<P, M>
): Promise<ResponseType<P, M>> => {
    //...
};

So now the types P and M are tied together.

Bonus

Finally, in case there is no parameter needed, I made the parameter params optional using a conditional type once again:

export const apiCall = <P extends Path, M extends PathMethods<P>>(
  path: P,
  method: M,
  ...params: RequestParams<P, M> extends undefined ? []: [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
    //...
};

Here is a working typescript playground with the solution. I added a method delete with no params just to test the final use case.

Sources

Edit

Here the updated typescript playground with on errors.

Also, I saw that Alessio's solution only works for one path which is a bit limitating. The one I suggest has no errors and works for any number of paths.

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for this contribution! However see @alessio's answer - this still has some TS issues unfortunately :(
@aurbano here the updated solution : no errors and any number of paths supported
That's amazing! Thank you so much :D
Just in case anyone is browsing this in the future, ResponseType shouldn't use keyof if we want it to be the actual response object.
0

I checked Baboo's solution by following the link to his TypeScript playground. At line 57, the ResponseType type gives the following error:

Type '"responses"' cannot be used to index type 'paths[P][M]'.(2536)
Type '200' cannot be used to index type 'paths[P][M]["responses"]'.(2536)
Type '"schema"' cannot be used to index type 'paths[P][M]["responses"][200]'.(2536)

I did some work starting from that solution, and obtained the functionality required without errors, and using slightly simpler type definitions, which require less type params. In particular, my PathMethod type does not need any type param, and my RequestParams and ResponseType types need only 1 type param.

Here is a TypeScript playground with the full solution.

As requested in the comments by captain-yossarian, here is the full solution:

export interface paths {
  '/v1/some-path/:id': {
    get: {
      parameters: {
        query: {
          id: number;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
    post: {
      parameters: {
        body: {
          name: string;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
    delete: {
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
  };
}

type Path = keyof paths;
type PathMethod = keyof paths[Path];
type RequestParams<T extends PathMethod> = paths[Path][T] extends {parameters: any} ? paths[Path][T]['parameters'] : undefined;
type ResponseType<T extends PathMethod> = paths[Path][T] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[Path][T]['responses'][200]['schema'] : undefined;

export const apiCall = <P extends Path, M extends PathMethod>(
  path: P,
  method: M,
  ...params: RequestParams<M> extends undefined ? [] : [RequestParams<M>]
): Promise<ResponseType<M>> => {
  const url = path;
  console.log('params', params);

  return fetch(url, { method }) as any;
};

UPDATE:

In the comments, aurbano noted that my solution only worked if paths has only 1 key. Here is an updated solution that works with 2 different paths.

export interface paths {
  '/v1/some-path/:id': {
    get: {
      parameters: {
        query: {
          id: number;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
    post: {
      parameters: {
        body: {
          name: string;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
    delete: {
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
  };
  '/v2/some-path/:id': {
    patch: {
      parameters: {
        path: {
          id: number;
        };
        header: {};
      };
      responses: {
        /** OK */
        200: {
          schema: {
            id: number;
            name: string;
          };
        };
      };
    };
  };
}

type Path = keyof paths;
type PathMethod<T extends Path> = keyof paths[T];
type RequestParams<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {parameters: any} ? paths[P][M]['parameters'] : undefined;
type ResponseType<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[P][M]['responses'][200]['schema'] : undefined;

export const apiCall = <P extends Path, M extends PathMethod<P>>(
  path: P,
  method: M,
  ...params: RequestParams<P, M> extends undefined ? [] : [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
  const url = path;
  console.log('params', params);

  return fetch(url, { method: method as string }) as any;
};

apiCall("/v1/some-path/:id", "get", {
  header: {},
  query: {
    id: 1
  }
}); // Passes -> OK

apiCall("/v2/some-path/:id", "get", {
  header: {},
  query: {
    id: 1
  }
}); // Type error -> OK

apiCall("/v2/some-path/:id", "patch", {
  header: {},
  query: {
    id: 1
  }
}); // Type error -> OK

apiCall("/v2/some-path/:id", "patch", {
  header: {},
  path: {
    id: 1,
  }
}); // Passes -> OK

apiCall("/v1/some-path/:id", "get", {
  header: {},
  query: {
    id: 'ee'
  }
}); // Type error -> OK

apiCall("/v1/some-path/:id", "get", {
  query: {
    id: 1
  }
}); // Type error -> OK

apiCall("/v1/some-path/:id", "get"); // Type error -> OK

apiCall("/v1/some-path/:id", 'delete'); // Passes -> OK

apiCall("/v1/some-path/:id", "delete", {
  header: {},
  query: {
    id: 1
  }
}); // Type error -> OK

And here is an updated playground.

3 Comments

Please share reproducable example in your answer. TS playground links is not reliable
This looks really promising!! However it only seems to work when there is one path in the paths interface - I left only one to simplify the example but have a look at the updated playground in the question :(
Just for precision's sake, I updated my solution to work with no errors and multiple paths 1 hour before @baboo updated his solution and wrote that mine was limited.

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.