はじめに
TypeScriptにおいて、特定の定数群を定義するためにオブジェクトリテラルを使うことが一般的であるが、実践的な開発を進める中でその限界が明らかになるケースがある。特に、複雑なロジックや拡張性を求められる場面では、オブジェクトリテラルの柔軟性や型安全性に課題が浮き彫りになる。本記事では、オブジェクトリテラルにメソッドを付与したいというニーズに対してクラスベースの型定義で対応した実践的な事例を紹介する。
オブジェクトリテラルを使う際の課題
TypeScriptのオブジェクトリテラルはシンプルで便利である反面、ロジックを追加する際には不便である。オブジェクトリテラルは静的な値のみを持つため、メソッドや複雑な振る舞いを直接追加できない。また、型安全性に関しても制限があり、関連ロジックを別途ユーティリティ関数として実装する必要があるため、コードの凝集性が低下しやすい。
クラスベースで型定義するアプローチ
これらの課題を解決するために、定数としてクラスインスタンスを定義する方法が有効である。この手法を用いることで、オブジェクトリテラルの制約を克服し、より柔軟で高度なロジックを実現できる。
クラスベースの型定義では、定数をクラスの静的インスタンスとして定義し、コンストラクタをprivateに設定することで外部からの不要なインスタンス生成を防止できる。結果として、各定数がシングルトン的に振る舞うため、型安全性を保ちながらメソッドを直接持たせることが可能となる。
クラスベースの利点
クラスベースのアプローチの最大の利点は、型安全性とロジックの凝集性が向上する点である。クラスインスタンスが自身の型を明示的に示し、誤った割り当てを防ぐことができる。また、メソッドやプロパティをインスタンス内に直接定義できるため、関連するロジックが自然にカプセル化され、保守性も大幅に向上する。
実践的な使用例(じゃんけんゲーム)
具体的な例として、じゃんけんゲームの手を表すHandType
クラスを示す。
export class HandType {
private constructor(private readonly value: string) {}
static readonly Gu = new HandType("グー");
static readonly Choki = new HandType("チョキ");
static readonly Pa = new HandType("パー");
static values(): HandType[] {
return [HandType.Gu, HandType.Choki, HandType.Pa];
}
static fromText(text: string): HandType | null {
return HandType.values().find(hand => hand.value === text) || null;
}
toText(): string {
return this.value;
}
isStrongerThan(other: HandType): boolean {
return (
(this === HandType.Gu && other === HandType.Choki) ||
(this === HandType.Choki && other === HandType.Pa) ||
(this === HandType.Pa && other === HandType.Gu)
);
}
}
パフォーマンス面での考察
クラスベースのアプローチはオブジェクトリテラルよりも若干のメモリ使用量増加が考えられるが、定数のインスタンス数が限られている場合、実用上の影響は微々たるものである。また、メソッド呼び出しなどの動作についても現代のJavaScriptエンジンの最適化が効くため、パフォーマンスへの影響は実質的に問題ない。
注意すべきデメリット
一方で、クラスベースのアプローチではコードが冗長化する可能性がある。オブジェクトリテラルに比べてクラスやメソッドの定義が必要になるため、コード量が増加し、導入時にチーム内で一定の学習コストが生じる可能性がある。
ServerからClientコンポーネントへの値渡し
ReactやNext.jsなどのフレームワークにおいては、ServerコンポーネントからClientコンポーネントへクラスインスタンスを直接渡すことは推奨されない。これはクラスインスタンスがJSONでシリアライズできないためである。そのため、サーバーからクライアントへ値を渡す際にはプリミティブ型やJSONなどシリアライズ可能な形式で渡し、クライアント側で再度インスタンス化する設計が望ましい。
オブジェクトリテラルとクラスベースの使い分け指標
プロジェクトの要求に応じて以下のように使い分けられる。
- 単純な定数定義が必要 → オブジェクトリテラル
- 複雑なロジックや拡張性が求められる → クラスベース
- 保守性や型安全性を重視 → クラスベース
- コードの簡潔さや導入コストを重視 → オブジェクトリテラル