Home > database >  Cannot use a property to index a generic type which should always have that property
Cannot use a property to index a generic type which should always have that property

Time:12-01

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;
}

Playground link

  • Related