1

I need to loop through an enum type to populate some options in a react component. Find below the enum and the function that retunrns the keys and values from it.

export enum ProyectType {
  time, 
  hits, 
  value, 
  results
}

function projectTypeValues() {
  const values = Object.keys(ProyectType).filter(k => typeof ProyectType[k as any] === "number"); // ["time", "hits", "value", "results"]
  const keys = values.map(k => ProyectType[k as any]); // [0, 1, 2, 3]
}

I dont like the any type in the ProyectType[k as any] so I tried:

type EnumIndex = number | string;
ProyectType[k as EnumIndex]

But I obtain: Element implicitly has an 'any' type because index expression is not of type 'number'.ts(7015)

I thought the indexer could be of type number or string, as the the Object.Keys is an array of the 8 elements: ["0","1","2","3","time","hits","value","results"] but neither of both works.

How can the any type be removed in this situation if the enum types are already known?

rustyBucketBay
  • 4,320
  • 3
  • 17
  • 47
  • Can't you use any of these solutions instead? https://stackoverflow.com/questions/43100718/typescript-enum-to-object-array – Buszmen Apr 13 '21 at 21:48

3 Answers3

5

The main issue here is that the type signature for Object.keys(obj) has the return type as string[] and not something like Array<keyof typeof obj>.

This is intentional: object types in TypeScript are open and not closed; they must some set of known properties, but they might have additional properties. See the Q/A pair "Why doesn't Object.keys return a keyof type in TypeScript?" for more details.

So the compiler sees you trying to index into ProyectType with an arbitrary string, and isn't happy about it. In cases where you just don't care about the possibility of extra keys, or if you happen to know what the keys will be, you can use a type assertion to just tell the compiler what it can't figure out.

For example, you could do this:

type ProyectTypeKeys = Array<keyof typeof ProyectType | number>;
// type ProyectTypeKeys = (number | "time" | "hits" | "value" | "results")[]

const values = (Object.keys(ProyectType) as ProyectTypeKeys).
    filter((k): k is keyof typeof ProyectType =>
        typeof ProyectType[k] === "number");
// const values: ("time" | "hits" | "value" | "results")[]

const keys = values.map(k => ProyectType[k]);
// const keys: ProyectType[]

Here I am defining ProyectTypeKeys as an array type whose elements are either the known string keys of the enum object or number... this is what you expect to see, and why you're doing the filter() step.

Similarly, I have annotated the filter() callback as a user-defined type guard function so that the compiler uses a call signature for filter() that narrows the type of the output of filter() from the full array type ProyectTypeKeys to just the type that comes out: keyof typeof ProyectType, a.k.a., "time" | "hits" | "value" | "results".

After this, the inferred type of keys is ProyectType[], as desired.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
1

Yeah it's really weird, it seems to happen just in some functions.

take a look in this approach:

const keys = [];
const values = [];
for(const index in ProyectType){
   //typescript infers string to index const  but here there is no error
  typeof ProyectType[index] === "number" ? values.push(index) : keys.push(index)
 
}

One possible solution to use the way that you wrote your code without error is using the unknown type + number.

const values = Object.keys(ProyectType).filter(k => typeof ProyectType[k as unknown as number] === "number"); // ["time", "hits", "value", "results"]
  • I try to avoid any and unknow as much as possible and the accepted answer approach seems more correct (`(k): k is keyof typeof ProyectType`). That was what I was looking for, however yours is also a useful snippet, thanks for your answer. – rustyBucketBay Apr 14 '21 at 14:54
1

Using Type Assertions

This is an elegant and alternative way ;) Considering that enums can contain only string and number types and your action is intentional, you can use type assertions to obtain your values and keys. Type Casting (or assertion) is a way to say to the compiler that you know what are you doing. You can find more info at this link.

In your case, the function should be like this:

function projectTypeValues() {
  const values = Object.values(ProyectType)
                .filter((val) => isNaN(val as number));
  const keys = Object.keys(ProyectType)
              .filter(key => !isNaN(key as unknown as number))
              .map(key => +key);
}

I used key as unknown as number because in this case our keys are string. Then I mapped them to convert strings to numbers as you wrote.