0

I am trying to set an object with a ternary operator based on a config value and an enum

import { config } from 'src/config'
import {logLevelEnum} from 'a-package-installed'

const someObject = {
  logLevel: config.logLevel ? logLevelEnum[config.logLevel] : logLevelEnum.NOTHING,
}

The enum is basically this:

export enum logLevelEnum {
  NOTHING = 0,
  ERROR = 1,
  WARN = 2,
  INFO = 4,
  DEBUG = 5,
}

But I get the compilation error:

Element implicitly has an 'any' type because index expression is not of type 'number'.
logLevel: config.logLevel ? logLevelEnum[config.logLevel] : logLevelEnum.NOTHING,
                                         ~~~~~~~~~~~~~~~

But I don't understand why it says that the index expression is supposed to be number since is an enum.

Can someone explain to me why and how can I achieve what I need?

Much appreciated.

LECHIP
  • 81
  • 1
  • 8
  • 1
    Is this a typo while you declared `logLevel` but use it as `logLevelEnum`? – Ryan Le Aug 26 '21 at 10:06
  • 1
    your code does now show what `logLevelEnum` is and also not what `config.logLevel` is... I guess that `config.logLevel` is NOT a number, but should be – TmTron Aug 26 '21 at 10:06
  • What's the type of `config.logLevel`? – Guerric P Aug 26 '21 at 10:10
  • `config.logLevel` is a string, before I used TS it was working ok, maybe there was an implicit cast? Also about the `logLevel` being used wrong, I edited, my bad. I am "paraphrasing" the code for brevity – LECHIP Aug 26 '21 at 10:15

2 Answers2

0

The problem is that config.logLevel is of type string while there is actually only a subset of valid strings.

So declare config.logLevel as a union type: 'NOTHING' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'

This union type doesn't seem to be generatable from the enum according to this: Generic type to get enum keys as union string in typescript?

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • I am confused tho, if I make it a union of string, it would still be a `string` but the compiler is saying it should be a `number`? The other problem is that I do not control the types of the `config.level` so I am not sure what to do in that case. – LECHIP Aug 26 '21 at 10:25
  • Don't mind the error saying that it should be a `number` this is not true – Guerric P Aug 26 '21 at 10:26
  • If you don't control `config.logLevel` then cast the type like this: `logLevelEnum[config.logLevel as 'NOTHING' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG']` – Guerric P Aug 26 '21 at 10:30
0

Related Typescript Playground Example

Some basics

You can access your enum in different ways:

  1. logLevelEnum.WARN = 2: by the enum directly
  2. logLevelEnum['WARN'] = 2: via index operator
  3. logLevelEnum[2] = WARN: get the enum-name from an enum-value
    This works because typescript creates a reverse mapping

When you try to access an invalid enum, you get undefined at runtime. Typescript tries to avoid this situation and gives you a compile error when possible to avoid this:

  1. logLevelEnum.warning: Property 'warning' does not exist on type 'typeof logLevelEnum'.
  2. logLevelEnum['warning'] = undefined: Element implicitly has an 'any' type because index expression is not of type 'number'.
    • this is maybe a little confusing, as it seems to indicate that you can only use number as index - which is not true - see 2. above
    • but the basic statement is right: typescript cannot guarantee that this expression returns a valid enum, thus the type of this expression is any (and not logLevelEnum or logLevelEnum | undefined, etc.)
      Hint: you can see the type when you hover over the invalidStringIndex variable in the Typescript Playground Example
  3. logLevelEnum[999] = undefined:
    • unfortunately we don't get a compile error here, but it is obviously not a valid index
    • the type of this expression is string! But actually it can also be undefined.
      Hint: when you activate the typescript-compiler option noUncheckedIndexedAccess, then the type will be string|undefined which is more accurate

Answer to your question

As I understand your question

  • config.logLevel is of type string and not under your control.
    If it were under your control, the type should be logLevelEnum and everything would be easier: logLevelEnum[logLevelEnum] is then guaranteed to be a valid enum (at compile time)
  • you want to get a valid log-level: when config.logLevel is valid, you want to use it, otherwise you want to use logLevelEnum.NOTHING

So basically you need a function like this (which you call with config.logLevel):

function getValidLogLevelEnum(logLevelName: string): logLevelEnum {
  /**
   * we need the correct type so that typescript will allow the index access
   * note: we must cast the string to `keyof typeof logLevelEnum`
   *       which resolves to: "NOTHING" | "ERROR" | "WARN" | "INFO" | "DEBUG"
   *       i.e. all valid enum-names
   */
  const enumName = logLevelName as keyof typeof logLevelEnum;
  /**
   * now that we have the correct type of the enum-name, we can get the enum-value
   */
  const configEnum = logLevelEnum[enumName];
  /**
   * keep in mind, that we were cheating a little bit in the type-expression above
   * We told typesript that we are sure that enumName is a valid enum-name,
   * but actually it is just the value of the logLevelName string, which could be anything.
   * Thus, typescript now thinks that configEnum is of type logLevelEnum, but 
   * actually it is `logLevelEnum | undefined` (undefined when logLevelEnum is not a valid enum-name)
   * 
   * This is the reason why use the nullish coalescing operator (??):
   * see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing
   * 
   * so we return configEnum, or logLevelEnum.NOTHING (when configEnum is undefined) 
   */
  return configEnum ?? logLevelEnum.NOTHING;
}
TmTron
  • 17,012
  • 10
  • 94
  • 142