Make something like Java Enum with Typescript

I'm lonely with the TypeScript Advent calendar, so I'll introduce what I wrote earlier.

Enums also exist in Typescript, but I'm not good at defining functions or handling multiple values together. (There is nothing you can't do with namespace](https://qiita.com/ConquestArrow/items/5d885128f896844698dd#%E9%96%A2%E6%95%B0%E3%82%84enum%E3 % 82% 92% E6% 8B% A1% E5% BC% B5))) I usually use Java, so when I was dealing with Typescript, I wanted something like Java's Enum. So, I tried to express something like Java Enum in a class with Typescript. (Something like that) I'm sure there are people who do it for granted even if they aren't told, but I'll leave it in place of my memo.

Enum requirements

First, let's list what you want from an Enum-like class (hereinafter referred to as the Enum class).

--Can declare constants of a fixed type --If the type is decided, the completion in the IDE will work. --You can add methods to constants --Constants are invariant --Neither the constant itself nor the internal properties can be changed --Constants cannot be added from the outside --You can get a list of declared constants in an immutable state. --You can get a constant from a given variable

code

I would like to create an Enum class that meets the above requirements with a class that defines a color called Color. Suppose a constant has two properties, name, which represents the name of the color, and hex, which represents the color in hexadecimal.

Can declare constants of a fixed type

To meet this requirement, define an object of Enum class as an Enum class variable. This allows the constant type to be an Enum class. You can also force property declarations by creating objects through the constructor.

class Color {
  public static RED = new Color("red", "#ff0000")
  public static GREEN = new Color("green", "#00ff00")
  public static BLUE = new Color("blue", "#0000ff")

  public constructor(
    public name: string,
    public hex: string,
  ) { }
}

You can add methods to constants

Since the constant is an object of the Color class, there is no problem if you declare the instance method like a normal class.

class Color {
  public static RED = new Color("red", "#ff0000")
  public static GREEN = new Color("green", "#00ff00")
  public static BLUE = new Color("blue", "#0000ff")

  public constructor(
    public name: string,
    public hex: string,
  ) { }

  //Comparison with other constants
  equals(other: Color) {
    //This time the names are the same
    return this.name === other.name
  }

  //Representing a constant as a string
  toString() {
    return `${this.name}(${this.hex})`
  }

  //Define the required methods below
}

The constant is invariant

With the above code, you can change the declared RED etc. from the outside. Therefore, add the readonly modifier to make the object immutable. This will cause a compile error if you try to set a new object in Color.RED or Color.GREEN declared as a constant.

class Color {
  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  public constructor(
    public name: string,
    public hex: string,
  ) { }

  //Class method
  //Instance method
}

The immutability of the constant itself is guaranteed, but the properties of the constant, name and hex, can be changed. Therefore, make the property immutable as well. There are two ways to achieve this.

Add readonly to the property

Just like making a constant immutable, add readonly to the property to prohibit setting. This method can minimize the amount of description, but getter is fixed by the property name.

class Color {
  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  public constructor(
    public readonly name: string,
    public readonly hex: string,
  ) { }

  //Class method
  //Instance method
}

Set the property to private and implement getter

Make the property invisible to the outside and access it through the getter. This method increases the amount of description, but it improves robustness and maintainability because you can freely describe getter without directly accessing the property.

class Color {
  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  public constructor(
    private _name: string,
    private _hex: string,
  ) { }

  //When using get accessor
  public get name() {
    return this._name 
  }

  //Like a normal getter
  public getName() {
    return this._name 
  }

  //Class method
  //Instance method
}

You can choose either one, but this time we are dealing with constant and invariant ones, so the former seems to be appropriate.

Constants cannot be added from the outside

To achieve this requirement, make the constructor invisible (private) and prohibit the creation of objects of the Color class. Now if you try to create a new object of the Color class from the outside, you will get an error.

class Color {
  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  private constructor(
    public readonly name: string,
    public readonly hex: string,
  ) { }

  //Class method
  //Instance method
}

You can get a list of declared constants in an immutable state

Let's think about the realization in two ways.

Add to list from constructor

As with constant properties, make the list a private class variable so that it is not edited from the outside, and prepare your own getter for access from the outside. To add to the list, you can get the object declared by this in the constructor, so we will add this.

With this method, even if the number of constants increases, it is not necessary to describe the part for adding the list, so you can prevent forgetting to add the constant. However, you need to prepare your own getter. (If you use get accessor for getter, there is a problem that it overlaps with the property name)

class Color {
  private static _values = new Array<Color>()

  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  private constructor(
    public readonly name: string,
    public readonly hex: string,
  ) { 
    Color._values.push(this)
  }

  public get values() {
    return this._values
  }

  //Class method
  //Instance method
}

Add to list yourself

I don't know if there is a usage scene, but it is a pattern to declare the list by yourself. Use ʻimmutable.js to make the list immutable. ʻImmutable.js is a nice library for easily declaring immutable lists and maps. Detailed explanation is here

This way you don't have to pollute the constructor to add a list, and you don't even have to include a getter for external access, which gives you a better view of the whole code. However, if the number of constants increases, there is a problem that the part described in the list bulges, and it is easy to forget to add a constant.

import { List } from 'immutable'

class Color {

  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  public static readonly values = List.of(
    Color.RED, Color.GREEN, Color.BLUE
  )

  private constructor(
    public readonly name: string,
    public readonly hex: string,
  ) { }

  //Class method
  //Instance method
}

I like which one to use, but this time I will use the one to add in the constructor.

You can get a constant from a given variable

Since this is a requirement for a class, we define a method that returns a constant as a class method.

class Color {
  private static _values = new Array<Color>()

  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  private constructor(
    public readonly name: string,
    public readonly hex: string,
  ) { 
    Color._values.push(this)
  }

  public get values() {
    return this._values
  }

  //Get a constant from the name
  static fromName(name: string) {
    return this.values.find(color => color.name === name)
  }
  
  //Class method
  //Instance method

}

Summary

The code so far can be summarized as follows.

class Color {
  private static _values = new Array<Color>()

  public static readonly RED = new Color("red", "#ff0000")
  public static readonly GREEN = new Color("green", "#00ff00")
  public static readonly BLUE = new Color("blue", "#0000ff")

  private constructor(
    public readonly name: string,
    public readonly hex: string,
  ) { 
    Color._values.push(this)
  }

  static get values() {
    return this._values
  }
  
  static fromName(name: string) {
    return this.values.find(color => color.name === name)
  }
  
  equals(other: Color) {
    return this.name === other.name
  }

  toString() {
    return `${this.name}(${this.hex})`
  }

}

There is also the issue of how much man-hours are spent just dealing with constants, but considering maintainability and extensibility, I think it is very useful that type inference works and that methods can be written. Next, let's consider whether we can write smarter using type inference.

If you have any improvements to the code, please feel free to comment.

bonus

Achieve Java valueOf

The explanation of valueOf is here. In a nutshell, it means "search by the declared constant name and return the matching constant".

Let's try to get this by force under the premise that "constants are all uppercase". If you don't need to filter, you can just get it with Color [name].

class Color {
  //abridgement
  
  static valueOf(name: keyof typeof Color) {
    if(/^[A-x]+$/.test(name)) {
      return Color[name] as Color
    }
    return undefined
  }

}

Caution

If you get it with Color [name], you will get a compile error if name does not match the key of type of Color. (The top is an error, the bottom is OK)

image.png

Recommended Posts

Make something like Java Enum with Typescript
[Java] Branch enum with switch statement
[Java] Reduce if statements with Enum
Create a high-performance enum with fields and methods like Java with JavaScript
[Java] enum
Interface Try to make Java problem TypeScript 7-3
Implement something like a stack in Java
[Java] Make programs 10x faster with parallelStream
[First Java] Make something that works with Intellij for the time being
I tried to make Basic authentication with Java
Make SpringBoot1.5 + Gradle4.4 + Java8 + Docker environment compatible with Java11
[Java] Create something like a product search API
Increment behavior Try to make Java problem TypeScript 3-4
String operation Try to make Java problem TypeScript 9-3
Something about java
Make Java code coverage more comfortable with Jacoco 0.8.0
Make Java Stream line breaks nice with eclipse
Easy to make LINE BOT with Java Servlet
I want to do something like "cls" in Java
Initialization of for Try to make Java problem TypeScript 5-4
Make Calendar gadgets made with JavaFX compatible with Java SE 9
[Java basics] Let's make a triangle with a for statement
Quickly implement a singleton with an enum in Java
Try something like sharding with Spring's Abstract Routing DataSource!
I used to make nc (netcat) with JAVA normally
Install java with Homebrew
Make Blackjack in Java
Change seats with java
Install Java with Ansible
Comfortable download with JAVA
Switch java with direnv
Enum reverse map Java
Download Java with Ansible
Let's scrape with Java! !!
[Java] About enum type
Build Java with Wercker
Endian conversion with JAVA
[Beginner] Try to make a simple RPG game with Java ①
I want to make a list with kotlin and java!
I want to make a function with kotlin and java!
Study Java: Use Timer to create something like a bomb timer