Playing with Typescript Enums
In the evolving landscape of TypeScript development, adopting a modular approach aligns seamlessly with contemporary coding practices, especially when handling constructs like enums. Let’s delve into the practicalities and nuances of refactoring TypeScript enums, leveraging the power of module syntax, a paradigm shift from traditional namespace usage.
Printing Enums as Strings: The Modular Way
Consider the LocalActionValue enum:
export enum LocalActionValue {
SCAN = 'Scan',
DIAGRAM = 'Diagram',
ADVISE = 'Advise',
REPORT = 'Report',
DUMP = 'Dump'
}
To print an enum member, such as LocalActionValue.DIAGRAM, the process remains straightforward:
console.log(LocalActionValue.DIAGRAM); // Outputs 'Diagram'
This exemplifies direct member access, a staple in TypeScript’s enum handling.
Reverse Mapping: From String to Enum
Reversing the process, converting a string like ‘Diagram’ back to its enum equivalent, invites a more nuanced approach. Here, we explore a modular function that elegantly handles this conversion:
export function getEnumValueFromString(value: string): LocalActionValue | undefined {
for (const key in LocalActionValue) {
if (LocalActionValue[key as keyof typeof LocalActionValue] === value) {
return LocalActionValue[key as keyof typeof LocalActionValue];
}
}
return undefined;
}
In this function, getEnumValueFromString, encapsulates the logic within a standalone module, promoting reuse and maintainability.
Enhancing Enum Functionality with Modern Modules
Historically, namespaces have been a go-to for attaching additional functionality to enums. However, in the realm of ES2015 and beyond, a more refined approach is to utilize module exports. Doing so adheres to contemporary styling preferences and enhances code clarity and separation of concerns.
Here’s an adaptation using module syntax:
export enum LocalActionValue {
SCAN = 'Scan',
DIAGRAM = 'Diagram',
ADVISE = 'Advise',
REPORT = 'Report',
DUMP = 'Dump'
}
export const getLocalActionValueKeyByValue = (value: string): LocalActionValue | undefined {
for (const key in LocalActionValue) {
if (LocalActionValue[key as keyof typeof LocalActionValue] === value) {
return LocalActionValue[key as keyof typeof LocalActionValue];
}
}
return undefined;
}
This module now cleanly exports the enum and its associated utility function, allowing consumers to import only what they need.
A Generic Approach: Versatile Enum Functions
Creating a generic function for enum-string conversions exemplifies the power of modular programming. Such a function, applicable to any enum, significantly elevates code reusability:
export function getEnumKeyByValue<E>(enumObj: E, value: string): keyof E | undefined {
const keys = Object.keys(enumObj) as Array<keyof E>;
for (const key of keys) {
if (enumObj[key] === value) { // a more robust comparison shown later
return key;
}
}
return undefined;
}
// Usage with LocalActionValue enum
const enumKey = getEnumKeyByValue(LocalActionValue, 'Diagram');
This generic approach, a testament to TypeScript’s flexibility, underscores the language’s capability to cater to a wide range of use cases with elegant, type-safe solutions.
Combining Approaches for Maximum Flexibility
While the modular approach is favored in modern TypeScript development, combining it with namespace-like patterns can sometimes yield the best of both worlds. This hybrid strategy allows for function-specific customizations while still enjoying the benefits of generic solutions:
// see below for source of this import
import { getEnumKeyByValue } from '../../utils/enum.util.js'
export enum LocalActionValue {
SCAN = 'Scan',
DIAGRAM = 'Diagram',
ADVISE = 'Advise',
REPORT = 'Report',
DUMP = 'Dump'
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace LocalActionValue {
export function getKeyByValue(
value: string
): keyof typeof LocalActionValue | undefined {
return getEnumKeyByValue(LocalActionValue, value)
}
}
A note about terminology: It’s important to note that in TypeScript 1.5, the nomenclature has changed. “Internal modules” are now “namespaces”. “External modules” are now simply “modules”, as to align with ECMAScript 2015’s terminology, (namely that
module X {
is equivalent to the now-preferrednamespace X {
). — https://www.typescriptlang.org/docs/handbook/namespaces.html
This method maintains a logical grouping of functionality, pertinent to specific enums, while leveraging generic underpinnings.
tl;dr; — you get to have your cake and eat it too!
import { LocalActionValue } from './enums/local-action.choice.js'
LocalActionValue.getKeyByValue('Diagram')
Handling Diverse Enum Types
In scenarios involving enums with non-string values, our generic function requires slight tweaks to accommodate such variations:
Suppose we have a file called enum.util.ts with this souce code
export function getEnumKeyByValue<E extends Record<string, any>>(
enumObj: E,
value: any
): keyof E | undefined {
const keys = Object.keys(enumObj).filter(key => isNaN(Number(key))) as Array<
keyof E
>
for (const key of keys) {
if (isObject(value) && isObject(enumObj[key])) {
if (deepEqual(value, enumObj[key])) {
return key
}
} else if (enumObj[key] === value) {
return key
}
}
return undefined
}
function isObject(obj: any): boolean {
return obj != null && typeof obj === 'object'
}
function deepEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) {
return true
}
if (isObject(obj1) && isObject(obj2)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const keys1 = Object.keys(obj1)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (const key of keys1) {
if (!keys2.includes(key)) {
return false
}
if (!deepEqual(obj1[key], obj2[key])) {
return false
}
}
return true
}
return false
}
This revision ensures our function remains universally applicable, regardless of the enum’s underlying data type, cementing TypeScript’s role as a versatile tool in modern software engineering.
Wrapping things up here, embracing module syntax in TypeScript, especially for enum handling, aligns with current best practices and provides a more intuitive, maintainable, and flexible coding experience. While subtle, this shift represents a significant stride in the evolution of TypeScript development methodologies. Take the best offerings in the language and make them work for you. While I wrote this originally as a note to myself I hope if you have read this far you get some value from it.