Java
JavaScript
TypeScript
enum

TypescriptでJavaのEnumのようなものを作る

TypeScriptのアドベントカレンダーがさみしいので前に書いたのを紹介します。

TypescriptにもEnumは存在しますが、関数を定義したり、複数の値をまとめて扱うことは苦手です。(namespaceを使ってできないこともないですが )
筆者が普段はJavaを使ってることもあり、Typescriptを扱っているとJavaのEnumっぽいものほしいなと思っていました。
そこで、TypescriptでJavaのEnumのようなものをクラスで表現してみました。(あくまでもようなもの)
言われなくても当たり前にやっている方もいらっしゃると思いますが、自分のメモ代わりに残しておきます。

Enumの要件

まず自分がEnumっぽいクラス(以下Enumクラス)に求めることをリストアップしてみます。

  • 決められた型の定数を宣言できる
    • 型が決まっていればIDEでの補完が効く
  • 定数にメソッドを追加できる
  • 定数は不変である
    • 定数自体も、内部のプロパティも変更できない
  • 外部からは定数を追加できない
  • 宣言されている定数のリストを不変な状態で取得できる
  • 与えられた変数から定数を取得できる

コード

上記の要件を満たすEnumクラスをColorという色を定義するのクラスで作ってみたいと思います。
定数は色の名前を表すnameと、色を16進数で表したhexの2つのプロパティを持つこととします。

決められた型の定数を宣言できる

この要件を満たすために、 EnumクラスのオブジェクトをEnumクラス変数として定義します。
これにより定数の型をEnumクラスとすることができます。
また、コンストラクタを通じてオブジェクトを生成することで、プロパティの宣言を強制することができます。

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,
  ) { }
}

定数にメソッドを追加できる

定数はColorクラスのオブジェクトなので、通常のクラスのようにインスタンスメソッドを宣言すれば問題ありません。

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,
  ) { }

  // 他の定数との比較
  equals(other: Color) {
    // 今回は名前で同一とする
    return this.name === other.name
  }

  // 定数を文字列として表現
  toString() {
    return `${this.name}(${this.hex})`
  }

  // 以下必要なメソッドを定義
}

定数は不変である

上記のコードでは宣言したREDなどを外から変更できてしまいます。そこで、オブジェクトを不変にするためにreadonly修飾子をつけます。これにより定数として宣言したColor.REDColor.GREENに新たにオブジェクトをセットしようとするとコンパイルエラーが発生するようになります。

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,
  ) { }

  // クラスメソッド
  // インスタンスメソッド 
}

定数自体の不変性は担保されましたが、定数のプロパティであるnamehexは変更できてしまいます。そこで、プロパティも変更不可にします。実現方法としては以下の2つがあります。

プロパティにreadonlyをつける

定数を不変にしたようにプロパティにもreadonlyをつけてセットを禁止します。この方法は記述量を最小に抑えることができますが、getterはプロパティ名で固定されてしまいます。

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,
  ) { }

  // クラスメソッド
  // インスタンスメソッド 
}

プロパティをprivateにし、getterを実装する

プロパティを外からは不可視にして、getterを通じてアクセスするようにします。この方法では記述量は増えますが、プロパティに直接アクセスせず、getterを自由に記述できるので堅牢性、保守性が上がります。

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,
  ) { }

  // getアクセサをつかう場合
  public get name() {
    return this._name 
  }

  // 通常のgetterっぽく
  public getName() {
    return this._name 
  }

  // クラスメソッド
  // インスタンスメソッド 
}

どちらを選ぶかは自由ですが、今回は定数で不変のものを扱っているので前者が適切かと思われます。

外部からは定数を追加できない

この要件を実現するためにコンストラクタを不可視(private)にしてColorクラスのオブジェクトの生成を禁止します。
これで外から新しくColorクラスのオブジェクトを作ろうとするとエラーになります。

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,
  ) { }

  // クラスメソッド
  // インスタンスメソッド 
}

宣言されている定数のリストを不変な状態で取得できる

こちらも二通りの方法で実現を考えてみます。

コンストラクタからリストに追加する

定数のプロパティと同じように外から編集されないようにリストをprivateなクラス変数とし、外からのアクセスは独自のgetterを用意します。
リストへの追加は、コンストラクタ内ではthisで宣言されたオブジェクトを取得できるので、こちらを追加していきます。

この方法では定数が増えてもリスト追加の部分を記載する必要がないため、定数の追加忘れを防ぐことができます。ただしgetterを独自に用意する必要があります。(getterをgetアクセサを使うとプロパティ名とかぶる問題もあり)

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
  }

  // クラスメソッド
  // インスタンスメソッド 
}

自前でリストに追加する

利用シーンがあるか分かりませんが、リストを自分で宣言するパターンです。
リストを不変にするためにimmutable.jsを使います。
immutable.jsは簡単にイミュータブルなリストやマップを宣言できる素敵なライブラリです。詳しい説明はこちら

この方法ではリスト追加のためにコンストラクタを汚さずに済み、外からのアクセスのためのgetterも記載しなくてよいため、コード全体の見通しがよくなります。しかし、定数が多くなるとリスト記載部分がふくれあがるといった問題があり、定数の追加忘れも発生しやすくなります。

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 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
  }

  // 名前から定数を取得
  static fromName(name: string) {
    return this.values.find(color => color.name === 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)
  }

  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})`
  }

}

定数を扱うことだけにどれほど工数をかけるかという問題もありますが、保守性や拡張性を考えると型推論が効くことと、メソッドが書けるというのは非常に有用だと思います。
次は型推論を使ってよりスマートに書けないか検討してみます。

コードの改善点などありましたら、是非是非コメントください。

おまけ

JavaのvalueOfを実現する

valueOfの説明は こちら
一言で説明すれば「宣言されている定数名で検索して一致した定数を返す」というものです。

これを「定数は全部大文字である」という前提の元、無理矢理取得してみます。
フィルターする必要がなければ、Color[name] でそのまま取得できます。

class Color {
  // 省略

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

}

注意

Color[name]で取得する場合、namekeyof typeof Colorに一致しないとコンパイルエラーがでます。(上はエラーで、下はOK)

image.png