I am trying to find a relatively generic way to type POST bodies and the responses I get back in conjunction with their API routes (in a nextjs app).
For this I want the compiler to force me to add a body
type and a return
type to all the API routes, which I achieved with the following interface:
export interface PostTypeMapping extends Record<string, {body: unknown, return: unknown}> {
"/api/taskCompletions": {body: PostCompletionBody, return: void},
"/api/task": {body: PostTaskBody, return: void},
}
So far so good. I can use this type in my API route like so:
async (req, res: NextApiResponse<PostTypeMapping["api/task"]["return"]>) => {
//...
}
But when I try to write a wrapper that automatically infers the POST body and the return type from the URL, I get an error in the line with await fetch(url,
:
Argument of type 'keyof PostTypeMapping' is not assignable to parameter of type 'RequestInfo'. Type 'number' is not assignable to type 'RequestInfo'
export async function fetchPost<T extends keyof PostTypeMapping>(url: T, body: PostTypeMapping[T]["body"]): Promise<PostTypeMapping[T]["return"]> {
try {
const res = await fetch(url, { // <- The error above occurs here
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
});
if(res.status === 201 || res.status === 204){
return;
}
return res.json();
} catch (err: any){
return {message: "Fehler: " + err.message};
}
}
Why can url
, which is typed as keyof PostTypeMapping
, be a number?
I investigated further and for the most part, the extends Record<string, {body: unknown, return: unknown}>
does seem to do what I want (force me to add a body and return type to all the entries of the interface), but allows numeric keys as well as strings. Why? Both cases that are not allowed are good.
export interface PostTypeMapping extends Record<string, {body: unknown, return: unknown}> {
"/api/taskCompletions": {body: PostCompletionBody, return: void},
"/api/task": {body: PostTaskBody, return: void},
1: {body: void, return: void}, // why is this legal? 1 is not a string
2: "asd" // not allowed -> Property '2' of type '"asd"' is not assignable to 'string' index type '{ body: unknown; return: unknown; }'
"asd": "asd" // not allowed -> Property '"asd"' of type '"asd"' is not assignable to 'string' index type '{ body: unknown; return: unknown; }
}
EDIT:
A simplified reproduction of the problem can be found at https://tsplay.dev/Nr5X2w thanks to T.J. Crowder
` iff `P` is assignable to `K`), and the inconsistency cannot be eliminated.