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.RED
やColor.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,
) { }
// クラスメソッド
// インスタンスメソッド
}
定数自体の不変性は担保されましたが、定数のプロパティであるname
やhex
は変更できてしまいます。そこで、プロパティも変更不可にします。実現方法としては以下の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]
で取得する場合、name
がkeyof typeof Color
に一致しないとコンパイルエラーがでます。(上はエラーで、下はOK)