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.
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
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.
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,
) { }
}
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
}
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.
readonly
to the propertyJust 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
}
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.
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
}
Let's think about the realization in two ways.
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
}
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.
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
}
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.
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)
Recommended Posts