Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a Typescript `Record<>` type with numeric enums

Tags:

In Typescript, it is possible to create record types with string enums:

enum AxisLabel { X = "X", Y = "Y" }
export const labelLookup: Record<AxisLabel, string> = {
  [AxisLabel.X]: "X axis",
  [AxisLabel.Y]: "Y Axis"
};

I need to create a Record object similar to the one above, but I do not wish to use a string enum.

When I try something like this:

enum AxisLabel { X, Y }
export const labelLookup: Record<AxisLabel, string> = {
  [AxisLabel.X]: "X axis",
  [AxisLabel.Y]: "Y Axis"
};

Typescript produces the following error:

Type 'AxisLabel' does not satisfy the constraint 'string'.

It is possible to create JS objects with both numbers and strings as their member names.

I wish to do the same in Typescript, but without resorting to unsafe coercion or type casts. How do I do create numeric enum Record<> types in Typescript without using string enums, any or type casts?

like image 443
Rick Avatar asked Dec 12 '17 15:12

Rick


People also ask

Can I use enum as a type in TypeScript?

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript. Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.

What is enum data type in TypeScript?

In TypeScript, enums, or enumerated types, are data structures of constant length that hold a set of constant values. Each of these constant values is known as a member of the enum. Enums are useful when setting properties or values that can only be a certain number of possible values.

Can we change enum values in TypeScript?

An enum is usually selected specifically because it is immutable - i.e. you would never want the values to change as it would make your application unpredictable.


2 Answers

UPDATE: TypeScript 2.9 added support for number and symbol as valid key types, thus the code above no longer gives an error, and this answer is no longer necessary, as long as you're using TypeScript 2.9 or above.

For TypeScript 2.8 and below:


Object keys in JavaScript are, believe it or not, always strings (okay, or Symbols), even for arrays. When you pass a non-string value as a key, it gets coerced into a string first. But of course people expect numeric keys to make sense, especially for arrays. TypeScript kind of reflects this inconsistent philosophy: usually you can only specify string-valued keys (such as in mapped types like Record<K,V>). When those situations interact you get weirdness.

Here's one thing I've sometimes done: explicitly represent the coercion from number to string with the following tuple type:

export type NumericStrings = ["0","1","2","3","4","5","6","7","8","9","10"] // etc

Note that you can extend that as long as you need it. Then, you can use lookup types to convert a numeric type to its string counterpart for use in mapped types. Like so:

export enum AxisLabel { X, Y }

// note the key is NumericStrings[AxisLabel], not AxisLabel    
export const labelLookup: Record<NumericStrings[AxisLabel], string> = {
  [AxisLabel.X]: "X axis",
  [AxisLabel.Y]: "Y Axis"
};

That works without error. If you inspect the type of labelLookup, it shows up as Record<"0" | "1", string>. When you try to index into labelLookup, the following mostly-expected things happen:

labelLookup[AxisLabel.X]; // okay
labelLookup[0]; // okay
labelLookup["0"]; // also okay

labelLookup[10]; // error
labelLookup.X; //error

Hope that helps; good luck!

like image 108
jcalz Avatar answered Oct 02 '22 07:10

jcalz


The definition of record is very specific that the key must be assignable to string, so there is no way to use Record with a number, more generally the key in a mapped type must be a string.

You can use a regular index signature, but it you cannot restrict an index signature to anything but string or number (according to the language spec) which means that any number would be valid not just the enum values:

export const labelLookup: { [index: number]: string } = {
    [AxisLabel.X]: "X axis",
    [AxisLabel.Y]: "Y Axis",
    [3] = "" // Also works but you don't want that
};
like image 32
Titian Cernicova-Dragomir Avatar answered Oct 02 '22 07:10

Titian Cernicova-Dragomir