Apperside
Apperside

Reputation: 3602

Typescript generics type inferring

I am playing with typescript by implementing a strongly typed rest request mechanism.

Let the code speak:

With this type I want to define the mapping between the routes and the related type of objects:

export interface RoutesMapping {
    api1: {
        users: UserApiModel
        products: ProductApiModel,
    }
    api2: {
        "other-route": OtherModel1,
        "another-one-route": OtherModel2
    }
}

export type ApiScope = keyof RoutesMapping

The following function is the one I am willing to use to make POST requests

export type RestApiPostResponse<T = any> = {
    result: boolean
    data: T
}

export function restPost<S extends keyof RoutesMapping = "api1", T extends keyof RoutesMapping[S] = keyof RoutesMapping[S]>(
  route: T,
  // nervermind this object, is out of scope for the question
  options: ApiRequestOptions<S, any> = {}
): Promise<RestApiPostResponse<RoutesMapping[S][T]>> {
  const url = apiUtils.buildUrl(route as string, options)
  const { payload, headers, isProtected } = options
  return post({
    url,
    isProtected,
    payload,
    headers
  });
}

I expect to call this function in the following way

const data = await restPost("users")

An make typescript infer the return type by inferring it by the scope and the route.

Actually, using it with the default type parameters, it works:

enter image description here

The problem is when I when I want to call the other api in this way:

const data = await restPost<"api2">("other-route")

Unfortunately, it does not work and it infers all the possible types

enter image description here

The only way to solve the problem is to explicitly add the second type parameter

enter image description here

How can I use all of this without needing to add the second type parameter in the second scenario?

Here is a typescript playground

Upvotes: 0

Views: 126

Answers (1)

Oblosys
Oblosys

Reputation: 15126

If you infer the api-key type parameter, you can actually construct a solution that does what you want:

type Model<Route> = // Returns the model value for key Route in RoutesMapping
  keyof RoutesMapping extends infer Api
  ? Api extends keyof RoutesMapping 
    ? Route extends keyof RoutesMapping[Api]
      ? RoutesMapping[Api][Route]
      : never
    : never
  : never

type Routes<Api> = Api extends {} ? keyof Api  : never // Helper to distribute keyof over a union of objects
type AllRoutes = Routes<RoutesMapping[keyof RoutesMapping]> // Union of all route keys: 'users' | 'products' | 'other-route' | 'another-one-route'

export function restPost<Route extends AllRoutes>(
  route: Route,
  options?:{url:string,payload:any}
): Promise<RestApiPostResponse<Model<Route>>> {
 ..
}

When applied to a route string, the correct return type for restPost is inferred, without needing to specify any type parameters:

const data = await restPost("users") // data: RestApiPostResponse<UserApiModel>
const data2 = await restPost("other-route") // data2: RestApiPostResponse<OtherModel1>

TypeScript playground

Note that this assumes the route keys are unique, which seems to be the case since the api key is not passed to restPost. I'm also not certain it is wise to introduce all this complexity, but at least it is possible.

Upvotes: 1

Related Questions