typescripttypesenums

Infer a strict key-value object type based on numeric enum in TypeScript


I have a a basic enum decribing log levels, it's variants are specifically made to be numbers to be able to conviniently handle log levels using <= operator

enum LogLevel {
  NONE,
  ERROR,
  WARNING,
  INFO,
  DEBUG,
}

I still have a need for string names of log levels to be able to display them in log messages, so I create a type, which can be indexed by LogLevel and will automatically fill an object fields with the names corresponding to LogLevel keys like so:

type LogLevelNames = {
  [K in LogLevel]: string;
};

// -- Issue here -> `string` can hold anything
const LOG_LEVEL_NAMES: LogLevelNames = {
  [LogLevel.NONE]: '',
  [LogLevel.ERROR]: '',
  [LogLevel.WARNING]: '',
  [LogLevel.INFO]: '',
  [LogLevel.DEBUG]: '',
};

It works, but as seen in example nothing obliges string values to have real meaningful names, let's change a type a bit:

export type LogLevelNames = {
  [K in LogLevel]: keyof typeof LogLevel;
};

// -- Another issue -> key/value types do not correlate
const LOG_LEVEL_NAMES: LogLevelNames = {
  [LogLevel.NONE]: "NONE",
  [LogLevel.ERROR]: "NONE",
  [LogLevel.WARNING]: "NONE",
  [LogLevel.INFO]: "NONE",
  [LogLevel.DEBUG]: "NONE"
};

Better already - the keys are now forced to match those of LogLevel, but they can match any of them

Here's where I'm stuck - it feels like I need to either have a way to infer a correlating type inside of an index signature, which I don't think is possible or have a way to apply keyof typeof to K in LogLevel context, which I didn't succeed in as well.

Question: Is there a way to achieve something like that?


Solution

  • For ease of discussion let's give a name to typeof LogLevel, the actual LogLevel object:

    type LogLevelObj = typeof LogLevel
    

    Now, that type is more or less what you're looking for, except that the keys are your desired values and the values are your desired keys. You can use key remapping in a mapped type to switch those, like this:

    type LogLevelNames = {
      [K in keyof LogLevelObj as LogLevelObj[K]]: K;
    };
    

    That is, for each key K of LogLevelObj whose value is LogLevelObj[K], the key of the new property is LogLevelObj[K] and the value is K. That gives you

    /* type LogLevelNames = {
        [x: string]: number;
        readonly 0: "NONE";
        readonly 1: "ERROR";
        readonly 2: "WARNING";
        readonly 3: "INFO";
        readonly 4: "DEBUG";
    } */
    

    Hmm, there's a string index signature in there. That's because numeric enums have reverse mappings and the type LogLevelObj has a numeric index signature like {[k: number]: string} to represent the reverse mapping. We really don't want that in our final type, because {[k: string]: number} messes up the rest of it.

    Let's modify LogLevelNames so that we skip the numeric index signature, by intersecting keyof LogLevelObj with string:

    type LogLevelNames = {
      [K in string & keyof LogLevelObj as LogLevelObj[K]]: K;
    };
    

    Now the type is

    /* type LogLevelNames = {
        readonly 0: "NONE";
        readonly 1: "ERROR";
        readonly 2: "WARNING";
        readonly 3: "INFO";
        readonly 4: "DEBUG";
    } */
    

    which is what you wanted.

    Playground link to code