Romain F.
Romain F.

Reputation: 794

Typescript: how to loop through an object indexed by enum values

I have an object declared like that:

type MyType = { [key in MyEnum]?: number}

MyEnum has that form:

enum MyEnum {
  Foo = 1, Bar = 2
}

I want to loop over my object key/value pairs, and get back to the enum value:

const myObject: MyType = {...}
for (const myKey in myObject) {
  // myKey is of type string, not "MyEnum". How to get back to it?
}

Is there any way to do that?

Upvotes: 1

Views: 3701

Answers (2)

enum MyEnum {
  Foo = 1, Bar = 2
}

type MyType = { [key in MyEnum]?: number}


declare var myObject: MyType;

for (const myKey in myObject) {
  // myKey is of type string, not "MyEnum". How to get back to it?
}

This made by design. Please be aware that Object.keys(MyEnum) will also return string[] instead of Array<keyof typeof MyEnum>.

In order to do that, you need to use type assertion - as operator.

enum MyEnum {
    Foo = 1, Bar = 2
}

type MyType = Record<keyof typeof MyEnum, 1>


declare var myObject: typeof MyEnum;


const loop =
    (Object.keys(MyEnum) as Array<keyof typeof MyEnum>)
        .forEach(elem => { // elem is Foo | Bar

        })

Playground

UPDATE

enum MyEnum {
    Foo = 1, Bar = 2
}

type MyType = Record<keyof typeof MyEnum, 1>


declare var myObject: typeof MyEnum;

type Values<T> = T[keyof T]
type O = {
    [Prop in Values<typeof MyEnum>]: Prop
}

const loop =
    (Object.values(MyEnum) as Array<Values<typeof MyEnum>>)
        .forEach((elem /** 1 | 2 */) => { })

Upvotes: 1

Fluous
Fluous

Reputation: 2485

This seemingly surprising issue is the consequence of duck typing in Typescript and to a lesser extent how objects work in Javascript.

The myKey is typed as a string, because of two reasons. Firstly, Typescript cannot guarantee that an object will only contain properties of MyType. Duck typing allows you to use objects that have the same and more properties as MyType on places where MyType is requested. This is a feature of Typescript, but in this case a handicap.

Example of duck typing

// Types
enum MyEnum {
  Foo = 1, Bar = 2
};

type MyType = { [key in MyEnum]?: number}

// Duck typing example
const normalObject: MyType = { [MyEnum.Foo]: 3, [MyEnum.Bar]: 4 };
const duckTypedObject = { [MyEnum.Foo]: 5, "extraProperty": 10};

function myFunction(obj: MyType){
  for (const myKey in obj) {
    console.log(myKey);
  }
}

myFunction(normalObject);
// This will print out "extraProperty" as well.
myFunction(duckTypedObject);

Second, because properties on objects in Javascript can only be symbols or strings other types are coerced into strings. Since the for loop will only iterate over the string properties of an object, Typescript chooses string as the most specific type for myKey.

Normally you could still type myKey by declaring a variable outside the loop construct. In this case that does not work, because you are using a numeric enum. You would need to parse the strings in order to get the numbers again and cast it back into an enum. I would highly advise against this. You could however use a string enum, but this would still expose you to errors if your colleagues would use duck typing.

String Enum Solution

enum MyStringEnum {
  Foo = "FOO", Bar = "BAR"
}

type MyType2 = { [key in MyStringEnum]?: number};
const obj: MyType2 = { [MyStringEnum.Foo]: 3};

let myKey: MyStringEnum;
for (const myKey in obj){
  console.log(myKey);
}

My suggestion would be to use the Map type from Typescript. As you can see the key is typed without any additional hacks and you evade issues with regard to duck typing.

enum MyEnum {
  Foo = 1, Bar = 2
};

let myMap = new Map<MyEnum, number>([
  [MyEnum.Foo, 3],
  [MyEnum.Bar, 4]
]); 

for (const [key] of myMap){
  console.log(key);
}

Here is a link to the executable code snippet.

Upvotes: 2

Related Questions