I am currently working on a method to standardize typing for POST bodies and their corresponding responses with API routes in my Next.js application.
To achieve this, I have created an interface that enforces the inclusion of a body
type and a return
type for all API routes:
export interface PostTypeMapping extends Record<string, {body: unknown, return: unknown}> {
"/api/taskCompletions": {body: PostCompletionBody, return: void},
"/api/task": {body: PostTaskBody, return: void},
}
Using this type definition allows me to implement it within my API routes as follows:
async (req, res: NextApiResponse<PostTypeMapping["api/task"]["return"]>) => {
//...
}
However, I encountered an error when attempting to create a wrapper function that automatically deduces the POST body and return types based on the URL. The error arises at the line containing 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: "Error: " + err.message};
}
}
How can the typed variable 'url' intended as 'keyof PostTypeMapping' be interpreted as a number?
Upon further investigation, I discovered that while the defined type ensures the inclusion of body and return types for each entry, it permits numeric keys alongside string keys. This raised the question of validity of such cases:
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 courtesy of T.J. Crowder