Objective
The goal is to make a fetch function which takes in the route and method as args, and the return type should be the type defined by the corresponding response
property from an interface called ScoresRoutes
.
In the ScoresRoutes
interface, the top level keys of the object can be any string - representing a route. Each of these keys should be an object which has any of a fixed set of strings - which would be the HTTP method. Lastly, each route and method combo should be an object which always has a response
property.
Behavior
My current solution does work when I go to use the custom fetch function I made. In the last line of the below code, the type of data
is the correct type - even when I add other routes and methods to the ScoresRoutes
interface - and TypeScript throws errors if I try to use customFetch
with an invalid route and method pair.
However, TypeScript still shows an error in the function definition, saying that Type '"response"' cannot be used to index type 'ScoresRoutes[R][M]'
.
I could just silence the error considering I have the functionality I want, but I imagine that the error is an indication that I'm going about this the wrong way.
Code
This is my broader type to describe the general shape of the desired interface:
type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
type GenericRoutes<Paths extends string> = {
[P in Paths]: {
[M in RequestMethod]?: {
request?: Record<string, unknown>,
response: Record<string, unknown> | null,
params?: Record<string, unknown>
}
}
}
Then here I have a specific implementation of it:
type ScoresPath = '/';
interface ScoresRoutes extends GenericRoutes<ScoresPath> {
'/': {
'GET': {
response: {
test: true
}
}
}
}
And lastly, this is the custom fetch method I've created:
async function customFetch<
R extends ScoresPath,
M extends keyof ScoresRoutes[R]
>(
route: R,
method: M
): Promise<ScoresRoutes[R][M]['response']> { // This is where the TS error shows
const data = await fetch(route, { method: method as string }).then((res) => res.json());
return data;
}
customFetch('/', 'GET').then((data) => null); // `data` does have the proper type here
CodePudding user response:
I believe you are getting this error because the response
field may not actually exist. This is because the object returns from ScoresRoutes[R][M]
may be undefined.
One way to work around this is to use a conditional type, which will allow you to check the type of object you're getting prior to accessing the response
property. Here is how you could do it:
type GenericRoutes<Paths extends string> = {
[P in Paths]: {
[M in RequestMethod]?: RequestMethodFields
}
}
// Extracted as a separate type
interface RequestMethodFields {
request?: Record<string, unknown>,
response: Record<string, unknown> | null,
params?: Record<string, unknown>
}
// New type; we no longer get an error accessing 'response' because we have already
// checked that we have the correct type of object
type MethodResponse<
R extends keyof ScoresRoutes,
M extends keyof ScoresRoutes[R]
> = ScoresRoutes[R][M] extends RequestMethodFields ? ScoresRoutes[R][M]['response'] : {}
async function customFetch<
R extends keyof ScoresRoutes,
M extends keyof ScoresRoutes[R]
>(
route: R,
method: M
): Promise<MethodResponse<R, M>> { // This is where the TS error shows
const data = await fetch(route, { method: method as string }).then((res) => res.json());
return data;
}