Home > Software engineering >  Infer rest parameters from first argument
Infer rest parameters from first argument

Time:01-03

I am unable of providing a solution creating the type that would infer the arguments of an ErrorMessage value based on the code argument.

./errorCodes.ts

export enum ErrorCodes {
  ErrorCode1,
  ErrorCode2,
  ErrorCode3
}

./errorMessages.ts

export const ErrorMessages = {
  [ErrorCodes.ErrorCode1]: (a1: string, a2: string) => `${a1} ${a2} message1...`,
  [ErrorCodes.ErrorCode2]: (a1: boolean) => `${a1} message2...`,
  [ErrorCodes.ErrorCode3]: `message3...`
}

./formatMessage.ts

import {ErrorCodes} from "./errorCodes"
import {ErrorMessages} from './errorMessages'


export const formatMessage = (
  code: ErrorCodes,
  ...args: Parameters<typeof ErrorMessages[typeof ErrorCodes[typeof code]]>
  // ^ Type '{ 0: (a1: string, a2: string) => string; 1: (a1: boolean) => string; 2: string; }' has no matching index signature for type 'string'.ts(2537)
) => {
  const message = ErrorMessages[code];
  const errorCode = `[${ErrorCodes[code]}]`;

  switch (typeof message) {
    case "string": {
      return [errorCode, message].join(" ");
    }
    case "function": {
      return [errorCode, message(...args)].join(" ");
      /**                              ^
       A spread argument must either have a tuple type or be passed to a rest parameter. ts(2556)
       */
    }
  }

I've tried the following line:

...args: Parameters<typeof ErrorMessages[typeof ErrorCodes[typeof code]]>

Even thought it semi-worked, there are some issues:

  1. There's no intellisense when invoking the function.
  2. as soon as I added string returns inside of ErrorMessages, the type was obviously broken.

CodePudding user response:

Here is how I would define the function:

export const formatMessage = <E extends ErrorCodes>(
  code: E,
  ...args: typeof ErrorMessages[E] extends infer Fn extends (...args: any) => any 
    ? Parameters<Fn> 
    : [typeof ErrorMessages[E]]
) => {
  const message = ErrorMessages[code];
  const errorCode = `[${ErrorCodes[code]}]`;

  switch (typeof message) {
    case "string": {
      return [errorCode, message].join(" ");
    }
    case "function": {
      return [errorCode, (message as (...args: any) => any)(...args)].join(" ");
    }
  }
}

Let's go over the changes.

  • The function should be generic. Something like

    typeof ErrorMessages[typeof ErrorCodes[typeof code]]
    

    probably does not what you expect it to do. typeof code will not reference the type of code with which the function was called. It only references its static type which is ErrorCodes. The whole statement just producess a union of all cases.

    We should instead give code the generic type E. The type of E is determined by the caller and can be referenced in the the second parameter type.

  • The second parameter has to use a conditional type to check if typeof ErrorMessages[E] is a function type before giving to to Parameters. As you already noticed, string can not be passed to this utility type because its not a function.

  • The last error message

    A spread argument must either have a tuple type or be passed to a rest parameter

    can not be solved with some satisfying solution. The compiler does not have the means to fully understand the higher order implications of the type of ...args and how it relates to the type of message. We have to help him out here by asserting that message is a function which can be called with whatever arguments.

    return [errorCode, (message as (...args: any) => any)(...args)].join(" "); 
    

With all these things in place, the function can now be called like this:

formatMessage(ErrorCodes.ErrorCode1, "a", "b")
formatMessage(ErrorCodes.ErrorCode2, true)
formatMessage(ErrorCodes.ErrorCode3, "a")

Playground

  • Related