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
}
But the second case looks more like this:
enum MyStringEnum {
first = 'first',
second = 'second',
third = 'third'
}
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'
}
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';
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));
}
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`);
}
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 = {};
Here, the type definition is the equivalent of:
type MyRecordType = {
first: unknown;
second: unknown;
third: unknown;
}
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 = {};
This is the equivalent of:
type MyPartialRecordType = {
first?: unknown;
second?: unknown;
third?: unknown;
}
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>>;
This is the equivalent of:
type MixedRecord = {
one: unknown;
two: unknown;
} & {
three?: unknown;
four?: unknown;
}
or, to simplify it further:
type MixedRecord = {
one: unknown;
two: unknown;
three?: unknown;
four?: unknown;
}
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>;
}
This is the equivalent of:
type ParamType = {
id: string;
name: string;
contentsOfWallet: unknown;
}
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
Top comments (0)