Search code examples
typescript

Index interface with template literal type and mapped type


I'm experimenting with TypeScript's type system to build an API class that's able to infer the expected data based on a passed string (which is a specific route on the backend).

However some of those routes include a number which makes my current implementation a bit ugly. I was wondering if you could improve that generic type.

My current implementation looks like this:

type ResourceType = 'api_keys' | 'message_forwardings' | `devices/${number}/user_devices`
type ResourceMap = {
  api_keys: IAkey,
  user_devices: IUserDevice
  message_forwardings: IMessageForwarding
}

type DataType<T extends ResourceType> =
    T extends keyof ResourceMap
        ? ResourceMap[T]
        : T extends `devices/${number}/user_devices`
            ? ResourceMap['user_devices']
            : never

class RestApiService<T extends ResourceType> {
    resource: T
    data: DataType<T>

    constructor(resource: T){
        this.resource = resource
    }

    // all the request methods go here
}

with this i can do:

const data = new RestApiService('api_keys').data // typescript knows it's Array<IAkey>

new RestApiService('some_route') // Argument of type 'some_route' is not assignable to parameter of type 'ResourceType'

const data = new RestApiService('devices/1234/user_devices').data // typescript knows it's Array<IUserDevice>

This is already working well but the more routes I want to add to the ResourceType and ResourceMap the more unclear my DataType narrowing can get, especially if there are even more routes that need a template literal type.

Is there a way to tidy things up and that I don't need to add more conditional types?


Solution

  • TypeScript supports template string pattern index signatures, so you can write your ResourceMap type directly as

    type ResourceMap = {
        api_keys: IAkey,
        [k: `devices/${number}/user_devices`]: IUserDevice
        message_forwardings: IMessageForwarding
    }
    

    and then do a pure indexed access instead of worrying about conditional types:

    type DataType<T extends ResourceType> = ResourceMap[T]
    

    Playground link to code