Pascal Precht
Pascal Precht

Reputation: 8893

How to query string enums in TypeScript strict mode?

TypeScript 2.8.4 in strict mode

I've got an enum like this:

export enum TabIndex {
  Editor = 'editor',
  Console = 'console',
  Settings = 'settings',
  Outputs = 'outputs'
}

Out of which I'm creating a Map like this:

tabs = new Map(<[string, string][]>Object.keys(TabIndex).map((key: any) => [TabIndex[key], key]));

Later on, I'm trying to get a particular member out of that Map using a query parameter which should be available in the Map:

const tabParam = this.urlSerializer.parse(this.location.path()).queryParams.tab; // this thign is either 'editor', 'console', 'settings', ... etc.

And then I do

if (this.tabs.has(tabParam)) { // at this point we know the tab is available
  this.selectTab(TabIndex[this.tabs.get(tabParam)!]); // here, TS thinks the index may be undefined, that's why I'm using the non-null assertion operator "!"
}

This code still makes TS unhappy. It errors out with:

Element implicitly has an 'any' type because index expression is not of type 'number'.

And it is true, the index type is a string. But I know that and that's how it's supposed to be because enums support string values. Anybody an idea how to make TS happy here?

I did some research and this issue comment suggests this workaround using keyof typeof:

const tabParam: keyof typeof TabIndex = this.urlSerializer.parse(this.location.path()).queryParams.tab;

This just makes TypeScript unhappy again:

Type 'string' is not assignable to type '"Editor" | "Console" | "Settings" | "Outputs"'

Upvotes: 0

Views: 2166

Answers (2)

KwintenP
KwintenP

Reputation: 4775

I think the problem is that you are giving your map the type: Map<string, string>. This is caused by the way you created your map: new Map(<[string, string][]> ...).

This is causing the error you are getting: Type 'string' is not assignable to type '"Editor" | "Console" | "Settings" | "Outputs"'

In the next snippet, you try to use the value of that Map by calling the get method with the key. This is however returning a string (as your Map's type has been defined as having values of type string.

this.selectTab(TabIndex[this.tabs.get(tabParam)!]);

The TabIndex[xxxx] statement however expects that the 'xxxx' is one of the following values "Editor" | "Console" | "Settings" | "Outputs" as you can deduct from the error message above.

To fix this, you need to change the type of your Map, so that typescript knows that the 'xxxx' in the snippet above will always be one of those values. To do that, you need to create a 'union types of those specific string literals'. Luckily, typescript gives us a way of extracting those from the TabIndex definition.

keyof typeof TabIndex

This will resolve to the 'union type of string literals' that is needed to properly type the Map.

So to sum up, change the creation of the map to:

const tabs = new Map(<[string, keyof typeof TabIndex][]>Object.keys(TabIndex).map((key: any) => [TabIndex[key], key]));

This will make sure the values from the Map that can be passed to TabIndex[xxxx] will always be a known string literal.

Upvotes: 3

Oscar Paz
Oscar Paz

Reputation: 18292

The problem is that, in the line defining typeParam, you are assigning a string (queryParam.tab is a string I imagine? Either that or any), to a keyof typeof TabIndex. string can contain any string value, so it can't be assignable to a variable that only accepts four specific values.

But, if you are sure that tab will always be what you expect you can make an assertion:

type TabIndexKey = keyof typeof TabIndex;
const tabParam: TabIndexKey = this.urlSerializer.parse(this.location.path()).queryParams.tab as TabIndexKey.

If queryParams.tab is any, the solution is the same.

EDIT: To avoid the error with not implicit any, index expressions for enums must use numbers, not strings. This means that:

TabIndex['editor']; // error with not implicit any
TabIndex[0]; // correct

So, I recommend you to change the definition of your enum and the tabs map. Instead of assigning strings, assign numbers:

export enum TabIndex {
  Editor = 0,
  Console = 1,
  Settings = 2,
  Outputs = 3
}

const tabs = new Map(<[string, number][]>Object.keys(TabIndex).map((key: any) => [key, TabIndex[key]]));

Of course, by doing this we are lying a little, because tabs is not really a <string, number> map. In reality it has both strings and numbers. The number keys point to the property names, and the string keys point to their number equivalents. However, we can forget it, as you are going to use just the string keys.

Now, TabIndex[this.tabs.get(tabParam)!] works correctly, as this.tabs.get returns a number.

Hope this helps you.

Upvotes: 0

Related Questions