Typescript: String Enums, the easy way

mandraketech

Navneet Karnani

Posted on June 5, 2021

Typescript: String Enums, the easy way

The most common use cases for an enum are:

  • Keys and associated non-string values
  • Keys, and string values that match the keys

Now, don't read this wrong. I just don't want to replicate everything that is written in the Typescript Handbook ( https://www.typescriptlang.org/docs/handbook/enums.html )

The first one is adequately handled in Typescript. Just by using:

enum MyEnum {
  first,
  second,
  third
}
Enter fullscreen mode Exit fullscreen mode

But the second case looks more like this:

enum MyStringEnum {
  first = 'first',
  second = 'second',
  third = 'third'
}
Enter fullscreen mode Exit fullscreen mode

As the number of values increase, it starts getting difficult to manage. And I see lot of boilerplate here. Also, there is scope for mistakes. For example, it is possible to get into this situation:

enum MyStringEnum {
  first = 'fifth',
  second = 'second',
  third = 'third'
}
Enter fullscreen mode Exit fullscreen mode

In the Handbook, look at all the complexity required to do a reverse lookup from the Enums.

Here is my proposal, to build a simple structure that you can implement quickly.

Lets start with defining the values we want to be the "keys" in the enum:

const VALID_ENUM_VALUES = ['first', 'second', 'third'] as const;

Notice the as const at the end of the statement. This is what will make the difference.

Lets define the type that we can use in the code, to ensure we are not using any invalid values:
type MyEnum = typeof VALID_ENUM_VALUES[number];

If you type this in VSCode, and hover your mouse over MyEnum, you should see that this is the equivalent of defining:
type MyEnum = 'first' | 'second' | 'third';

The [number] tells Typescript to get all the "number based subscripts" of the array.

The additional advantage, is, if you make changes to the VALID_ENUM_VALUES array, the MyEnum changes with it.

So, if you were to type the following code in the editor:

console.log("Valid values of the enum are:", VALID_ENUM_VALUES);
const valueToCheck = 'first';
console.log(`Check if '${valueToCheck}' is part of the enum`, VALID_ENUM_VALUES.includes(valueToCheck))

// Error here, because "hello" is not a value in the VALID_ENUM_VALUES array.
const typedVar: MyEnum = 'hello';
Enter fullscreen mode Exit fullscreen mode

Reverse lookups are not necessary. But, you do want a way to check if a given value is valid in the context of this Enum. For that, lets write a type asserter:

function isValid(param: unknown): asserts param is MyEnum {
    assert( param && typeof param === 'string' && VALID_ENUM_VALUES.includes(param as MyEnum));
}
Enter fullscreen mode Exit fullscreen mode

Now, in this context:

const myStr = 'first';
if ( isValid(myStr)) {
  // here, if 'myStr' is implicitly of type 'MyEnum'
  console.log(`${myStr} is a valid Enum value`);
}
Enter fullscreen mode Exit fullscreen mode

Another use of this construct, is in defining Objects with keys. Take a look:

type MyRecordType = Record<MyEnum, unknown>;

// the 'myValue' below will error, because '{}' is not a valid value
const myValue: MyRecordType = {};

Enter fullscreen mode Exit fullscreen mode

Here, the type definition is the equivalent of:

type MyRecordType = {
  first: unknown;
  second: unknown;
  third: unknown;
}
Enter fullscreen mode Exit fullscreen mode

You may change the 'unknown' to any relevant type. So, this gives you a quick way of defining objects with a given structure, and defined types. Obviously, more complex cases are better handled manually.

Here is another variation of the same:

type MyPartialRecordType = Partial<MyRecordType>;
// no error here
const myPartialValue: MyPartialRecordType = {};
Enter fullscreen mode Exit fullscreen mode

This is the equivalent of:

type MyPartialRecordType = {
  first?: unknown;
  second?: unknown;
  third?: unknown;
}
Enter fullscreen mode Exit fullscreen mode

If you want to use these in combination, try this:

const MUST_HAVE_PARAMS = ['one', 'two'] as const;
type MandatoryParams = typeof MUST_HAVE_PARAMS[number];
const OPTIONAL_PARAMS = ['three', 'four'] as const;
type OptionalParams = typeof OPTIONAL_PARAMS[number];
type MixedRecord = Record<MandatoryParams, unknown> & Partial<Record<OptionalParams, unknown>>;
Enter fullscreen mode Exit fullscreen mode

This is the equivalent of:

type MixedRecord = {
    one: unknown;
    two: unknown; 
} & {
    three?: unknown;
    four?: unknown;
}
Enter fullscreen mode Exit fullscreen mode

or, to simplify it further:

type MixedRecord = {
    one: unknown;
    two: unknown; 
    three?: unknown;
    four?: unknown;
}
Enter fullscreen mode Exit fullscreen mode

So, you can now create a Union type, Record type, and also have a array to validate the values against.

Another interesting example, involving Mapped Types:

const KNOWN_PARAMS_TYPES = ['id', 'name'] as const;
type KnownParams = typeof KNOWN_PARAMS_TYPES[number];

const UNKNOWN_PARAMS_TYPES = ['contentsOfWallet'] as const;
type UnknownParams = typeof UNKNOWN_PARAMS_TYPES[number];

type AllParams = KnownParams | UnknownParams;

type ValueType<T extends AllParams> = T extends KnownParams ? string : unknown;
type ParamType = {
    [Property in AllParams]: ValueType<Property>;
}
Enter fullscreen mode Exit fullscreen mode

This is the equivalent of:

type ParamType = {
    id: string;
    name: string;
    contentsOfWallet: unknown;
}
Enter fullscreen mode Exit fullscreen mode

This may look like a lot of magic for something that can be defined in less space, but look at what is available:

  • Arrays of valid field names, that can be used for input validation, for example when you are dealing with http query strings and want to check if the parameter name is valid
  • String union types for use within the application code, for those places where you would have otherwise used key of ParamType as the type
  • A structure that will update itself, as you add more parameters to the known / unknown parts.

In summary, for cases where you want an array of values to use in various places in the application, and still want type safe data structures, this kind of organisation will go a long way in making your code extensible, using the power of Typescript.

This blog was originally published by Navneet Karnani ( navneet@mandraketech.in ) on his blog at: https://blog.mandraketech.in/typescript-string-enums

💖 💪 🙅 🚩
mandraketech
Navneet Karnani

Posted on June 5, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

How I code?
programming How I code?

August 31, 2022

Getting Started with Index Signatures
todayilearned Getting Started with Index Signatures

April 13, 2022

More on Types in Typescript
beginners More on Types in Typescript

October 19, 2021