Home > Software design >  Typescript is it possible to infer types based on argument enum?
Typescript is it possible to infer types based on argument enum?

Time:05-23

Let's say that I have a function that has 3 arguments. The first argument is an enum that is defined by me, the second argument is an object that has the shape based on the value passed by the enum. The third argument is just a string.

Look at this function:

  async callApi<Response = any, Input = any>(
    op: ApiOperations,
    input: Input,
    functionName: string
  ): Promise<Response> {
    return this.lambda.invoke<Response>(functionName, {
      op,
      input
    });
  }

You can see that I'm declaring this function as generic and receiving two types and setting the default to any and that works. But in this case whenever I want to call this function I have to manually specify the input and response types. The thing is, I know that for each value from my ApiOperations enum, I have just one input type and one response type.

So my question is, is there any way of typescript can infer the types based on the enum value?

An example of calling this function is:

  async getChatRooms({ numberResults, siteId, startIndex }: GetChatRoomsInput): Promise<ChatRoom[]> {
    return this.api.callApi<ChatRoom[], GetChatRoomsInput>(ApiOperations.GetChatRooms, {
      siteId,
      numberResults,
      startIndex
    }, 'getChatRooms');
  }

This works fine, but the way that I want to do is:

  async getChatRooms({ numberResults, siteId, startIndex }: GetChatRoomsInput): Promise<ChatRoom[]> {
    return this.api.callApi(ApiOperations.GetChatRooms, {
      siteId,
      numberResults,
      startIndex
    }, 'getChatRooms');
  }

And for this case typescript would be able to tell me that the input has the GetChatRoomsInput type and ChatRoom[] as the response type.

CodePudding user response:

You just just need a lookup type that maps enum values to input/response types. For example:

enum ApiOperations {
  A,
  B,
}

interface ApiOperationsLookup {
  [ApiOperations.A]: {
    input: { a: number }
    response: { a: string }
  }

  [ApiOperations.B]: {
    input: { b: number }
    response: { b: string }
  }
}

Here ApiOperationsLookup is a type that has the key names of the ApiOperations mapped to specific input and response types.

You can fetch the input type with something like:

type Test = ApiOperationsLookup[ApiOperations.A]['input']
// { a: number }

Now you can make callApi look like this:

  async callApi<T extends ApiOperations>(
    op: T,
    input: ApiOperationsLookup[T]['input'],
    functionName: string
  ): Promise<ApiOperationsLookup[T]['response']> {
    //...
  }

Here the generic parameter T is a value from the ApiOperations, and then input and the return type are pulled from the lookup map for that operation.

This now works as I think you expect:

const aResponse = await instance.callApi(ApiOperations.A, { a: 123 }, 'aFuncName')
// { a: string }

const aBadResponse = await instance.callApi(ApiOperations.A, { b: 123 }, 'aFuncName')
// type error on second argument

const bResponse = await instance.callApi(ApiOperations.B, { b: 123 }, 'aFuncName')
// { b: string }

Playground


Another option is to skip the lookup type and instead use overloads where you create a function signature for each member of the enum:

  // signature for each enum member
  async callApi(op: ApiOperations.A, input: { a: number }, functionName: string): Promise<{ a: string }>
  async callApi(op: ApiOperations.B, input: { b: number }, functionName: string): Promise<{ b: string }>

  // implementation
  async callApi(
    op: ApiOperations,
    input: unknown,
    functionName: string
  ): Promise<unknown> {
    //...
  }

I think that's a big uglier and harder to maintain personally, but that's a matter of opinion.

Playground

  • Related