This spike explores various approaches to recreating a Java Enum
in TypeScript,
focusing on adhering to the key rules and characteristics of Java's enum system.
The goal is to analyze the strengths and weaknesses of each implementation, compare them,
and conclude with the best possible design to replicate Java enums in TypeScript.
- Understand the essentials rules of Java enums.
- Implement multiple approaches to mimicking a Java enum in TypeScript.
- Evaluate each approach based on adherence to Java enum principles.
- Conclude with recommendations for the best solution.
To ensure that our TypeScript implementations closely follow Java enums, we’ll adhere to the following key rules:
- Immutability: Enum values must be constant and immutable.
- Ordinal: Each enum constant must have a unique index reflecting its declaration order.
- Name: Each enum constant must have a string name representing its declared name.
- Type Safety: Only predefined constants should be allowed for a given enum type.
- Comparison: Enum constants must be comparable based on their ordinal index.
- Private Constructor: The constructor of the enum should be private to prevent the creation of new instances outside the predefined constants.
- Singleton: Each enum constant must be a singleton, meaning only one instance exists for each constant.
- Value Retrieval: The enum should provide a way to retrieve all constants (e.g.,
values()
method) and lookup constants by name (valueOf()
). - Custom Fields and Methods: Enum constants should support additional fields (e.g., descriptions) and custom methods if needed.
- Handling in Value Lookup: The
valueOf()
method should throw an appropriate exception if a name lookup fails ( e.g.,IllegalArgumentException
in Java).
We will create the Day
enum in multiple ways, representing the enumeration containing the days of the week.
Each implementation will be evaluated based on how well it adheres to the key rules of Java enums.
export enum DayTSEnum {
MONDAY = 'MONDAY',
TUESDAY = 'TUESDAY',
WEDNESDAY = 'WEDNESDAY',
THURSDAY = 'THURSDAY',
FRIDAY = 'FRIDAY',
SATURDAY = 'SATURDAY',
SUNDAY = 'SUNDAY'
}
export interface DayTSInterface {
readonly label: string;
readonly isWeekend: boolean;
}
const dayTSRecord: Record<DayTSEnum, DayTSInterface> = {
[DayTSEnum.MONDAY]: {label: 'Monday', isWeekend: false},
[DayTSEnum.TUESDAY]: {label: 'Tuesday', isWeekend: false},
[DayTSEnum.WEDNESDAY]: {label: 'Wednesday', isWeekend: false},
[DayTSEnum.THURSDAY]: {label: 'Thursday', isWeekend: false},
[DayTSEnum.FRIDAY]: {label: 'Friday', isWeekend: false},
[DayTSEnum.SATURDAY]: {label: 'Saturday', isWeekend: true},
[DayTSEnum.SUNDAY]: {label: 'Sunday', isWeekend: true}
};
export namespace DayTSEnumUtils {
export function label(day: DayTSEnum): string {
return dayTSRecord[day].label;
}
export function isWeekend(day: DayTSEnum): boolean {
return dayTSRecord[day].isWeekend;
}
export function name(day: DayTSEnum): string {
return day;
}
export function ordinal(day: DayTSEnum): number {
return Object.keys(DayTSEnum).indexOf(day);
}
export function toString(day: DayTSEnum): string {
return DayTSEnumUtils.name(day);
}
export function equals(day: DayTSEnum, other: unknown): boolean {
return day === other;
}
export function compareTo(a: DayTSEnum, b: DayTSEnum): number {
return DayTSEnumUtils.ordinal(a) - DayTSEnumUtils.ordinal(b);
}
export function valueOf(name: string): DayTSEnum {
const result = DayTSEnum[name as keyof typeof DayTSEnum];
if (null !== result) {
return result;
}
if (null === name) {
throw new TypeError('Name is null');
}
throw new Error(`No enum constant DayTSEnum.${name}`);
}
export function values(): DayTSEnum[] {
return Object.values(DayTSEnum);
}
}
- Immutability: The enum values are constant and cannot be changed.
- Ordinal: Each enum constant has a unique index based on declaration order.
- Name: Each constant has a string representation corresponding to its name.
- Type Safety: Only predefined constants of DayTSEnum can be used.
- Comparison: Supports comparison using the ordinal value.
- Value Retrieval: Provides utility functions for getting all constants and looking them up by name.
- Custom Fields: The DayTSInterface allows for additional properties like label and isWeekend.
- Private Constructor: There is no private constructor to prevent instantiation outside of the enum; TypeScript enums do not support constructors.
- Singleton Enforcement: While enums are singletons in usage, there's no explicit enforcement of this concept in the code.
- Lack of Encapsulation: The utility functions are all static, and they are not encapsulated within the enum itself.
Rule | Compliance |
---|---|
Immutability | ✅ Yes |
Ordinal | ✅ Yes |
Name | ✅ Yes |
Type Safety | ✅ Yes |
Comparison | ✅ Yes |
Private Constructor | ❌ No |
Singleton | ✅ Yes (implicit) |
Value Retrieval | ✅ Yes |
Custom Fields and Methods | ✅ Yes |
Handling in Value Lookup | ✅ Yes |
export const DayConstEnum = {
MONDAY: {label: 'Monday', isWeekend: false},
TUESDAY: {label: 'Tuesday', isWeekend: false},
WEDNESDAY: {label: 'Wednesday', isWeekend: false},
THURSDAY: {label: 'Thursday', isWeekend: false},
FRIDAY: {label: 'Friday', isWeekend: false},
SATURDAY: {label: 'Saturday', isWeekend: true},
SUNDAY: {label: 'Sunday', isWeekend: true}
} as const;
type DayConstEnum = typeof DayConstEnum[keyof typeof DayConstEnum];
export namespace DayConstEnumUtils {
export function name(day: DayConstEnum): string {
return Object.keys(DayConstEnum)[DayConstEnumUtils.ordinal(day)];
}
export function ordinal(day: DayConstEnum): number {
return Object.values(DayConstEnum).indexOf(day);
}
export function toString(day: DayConstEnum): string {
return DayConstEnumUtils.name(day);
}
export function equals(day: DayConstEnum, other: unknown): boolean {
return day === other;
}
export function compareTo(a: DayConstEnum, b: DayConstEnum): number {
return DayConstEnumUtils.ordinal(a) - DayConstEnumUtils.ordinal(b);
}
export function valueOf(name: string): DayConstEnum {
const result = DayConstEnum[name as keyof typeof DayConstEnum];
if (null !== result) {
return result;
}
if (null === name) {
throw new TypeError('Name is null');
}
throw new Error(`No enum constant DayConstEnum.${name}`);
}
export function values(): DayConstEnum[] {
return Object.values(DayConstEnum);
}
}
- Immutability: The use of
as const
ensures that the enum values are constant and immutable. - Ordinal: Each enum constant can be accessed by its index based on declaration order.
- Name: Each constant has a string representation corresponding to its name.
- Type Safety: Only predefined constants can be used since TypeScript ensures type safety through the
const
assertion. - Comparison: Supports comparison using the ordinal value.
- Value Retrieval: Provides utility functions for getting all constants and looking them up by name.
- Custom Fields: Each constant can have additional fields such as
label
andisWeekend
.
- Private Constructor: There is no private constructor to prevent instantiation; the structure is simply an object.
- Singleton Enforcement: While each constant is effectively a singleton, there’s no explicit enforcement in the implementation.
- No Built-in Enum Features: This solution lacks some inherent features of TypeScript enums, such as automatic numeric values.
Rule | Compliance |
---|---|
Immutability | ✅ Yes |
Ordinal | ✅ Yes |
Name | ✅ Yes |
Type Safety | ✅ Yes |
Comparison | ✅ Yes |
Private Constructor | ❌ No |
Singleton | ✅ Yes (implicit) |
Value Retrieval | ✅ Yes |
Custom Fields and Methods | ✅ Yes |
Handling in Value Lookup | ✅ Yes |
export class DayStaticClassEnum {
static readonly MONDAY = new DayStaticClassEnum('Monday', false);
static readonly TUESDAY = new DayStaticClassEnum('Tuesday', false);
static readonly WEDNESDAY = new DayStaticClassEnum('Wednesday', false);
static readonly THURSDAY = new DayStaticClassEnum('Thursday', false);
static readonly FRIDAY = new DayStaticClassEnum('Friday', false);
static readonly SATURDAY = new DayStaticClassEnum('Saturday', true);
static readonly SUNDAY = new DayStaticClassEnum('Sunday', true);
private constructor(readonly label: string, readonly isWeekend: boolean) {
}
get name(): string {
return Object.keys(DayStaticClassEnum)[this.ordinal];
}
get ordinal(): number {
return Object.values(DayStaticClassEnum).indexOf(this);
}
toString(): string {
return this.name;
}
equals(other: unknown): boolean {
return this === other;
}
compareTo(other: DayStaticClassEnum): number {
return this.ordinal - other.ordinal;
}
static valueOf(name: string): DayStaticClassEnum {
const result = DayStaticClassEnum[name as keyof typeof DayStaticClassEnum] as DayStaticClassEnum;
if (null !== result) {
return result;
}
if (null === name) {
throw new TypeError('Name is null');
}
throw new Error(`No enum constant DayStaticClassEnum.${name}`);
}
static values(): DayStaticClassEnum[] {
return Object.values(DayStaticClassEnum);
}
}
- Immutability: The
readonly
modifier ensures that the enum values cannot be changed after creation. - Ordinal: Each constant can be accessed by its index based on declaration order.
- Name: Each constant has a string representation corresponding to its label.
- Type Safety: Only predefined constants can be used, and types are strictly enforced through class instances.
- Comparison: Supports comparison using the ordinal value.
- Private Constructor: Prevents instantiation of new instances, ensuring the singleton nature of each constant.
- Value Retrieval: Provides utility functions for getting all constants and looking them up by name.
- Singleton Instances: Each constant is a separate instance, which may slightly increase memory usage compared to a simpler enum.
Rule | Compliance |
---|---|
Immutability | ✅ Yes |
Ordinal | ✅ Yes |
Name | ✅ Yes |
Type Safety | ✅ Yes |
Comparison | ✅ Yes |
Private Constructor | ✅ Yes |
Singleton | ✅ Yes |
Value Retrieval | ✅ Yes |
Custom Fields and Methods | ✅ Yes |
Handling in Value Lookup | ✅ Yes |
export abstract class Enum<E extends Enum<E>> {
get name(): string {
return Object.keys(this.constructor)[this.ordinal];
}
get ordinal(): number {
return Object.values(this.constructor).indexOf(this);
}
toString(): string {
return this.name;
}
equals(other: unknown): boolean {
return this === other;
}
compareTo(other: E): number {
return this.ordinal - other.ordinal;
}
static valueOf<T extends Enum<T>>(this: T, name: string): T | any {
const result = this[name as keyof typeof this] as T;
if (null !== result) {
return result;
}
if (null === name) {
throw new TypeError('Name is null');
}
throw new Error(`No enum constant ${this.constructor.name}.${name}`);
}
static values<T extends Enum<T>>(): T[] | any[] {
return Object.values(this);
}
}
import {Enum} from '../base/enum.base.ts';
export class DayAdvancedStaticClassEnum extends Enum<DayAdvancedStaticClassEnum> {
static readonly MONDAY = new DayAdvancedStaticClassEnum('Monday', false);
static readonly TUESDAY = new DayAdvancedStaticClassEnum('Tuesday', false);
static readonly WEDNESDAY = new DayAdvancedStaticClassEnum('Wednesday', false);
static readonly THURSDAY = new DayAdvancedStaticClassEnum('Thursday', false);
static readonly FRIDAY = new DayAdvancedStaticClassEnum('Friday', false);
static readonly SATURDAY = new DayAdvancedStaticClassEnum('Saturday', true);
static readonly SUNDAY = new DayAdvancedStaticClassEnum('Sunday', true);
private constructor(readonly label: string, readonly isWeekend: boolean) {
super();
}
static valueOf(value: string): DayAdvancedStaticClassEnum {
return super.valueOf<DayAdvancedStaticClassEnum>(value);
}
static values(): DayAdvancedStaticClassEnum[] {
return super.values<DayAdvancedStaticClassEnum>();
}
}
- Immutability: The use of
readonly
ensures that the enum values cannot be modified after creation. - Ordinal: Each constant can be accessed by its index based on declaration order, leveraging the base
Enum
class. - Name: Each constant has a string representation based on its label.
- Type Safety: Only predefined constants can be used, with strict type checking.
- Comparison: Supports comparison using the ordinal value.
- Private Constructor: The constructor is private, preventing the creation of new instances outside the defined constants.
- Value Retrieval: Inherits value retrieval methods from the base
Enum
class, enhancing consistency. - Custom Fields and Methods: Allows for additional fields (
label
,isWeekend
) and methods from theEnum
base class.
Rule | Compliance |
---|---|
Immutability | ✅ Yes |
Ordinal | ✅ Yes |
Name | ✅ Yes |
Type Safety | ✅ Yes |
Comparison | ✅ Yes |
Private Constructor | ✅ Yes |
Singleton | ✅ Yes |
Value Retrieval | ✅ Yes |
Custom Fields and Methods | ✅ Yes |
Handling in Value Lookup | ✅ Yes |
Advanced Static Class Enum - 10/10
Static Class Enum - 10/10
Const Enum - 9/10
Basic TypeScript Enum - 6/10
The Advanced Static Class Enum and Static Class Enum solutions are the most comprehensive in TypeScript and adhere to all feasible rules. The Const Enum is also an excellent alternative, although slightly less robust in terms of exception handling. The Basic TypeScript Enum remains functional but lacks advanced features such as custom fields and value management.