TypeScript Union 型でスキルのレンジ種別とダメージ計算を型安全に実装する

TypeScript Union 型でスキルのレンジ種別とダメージ計算を型安全に実装する ゲーム開発、特に RPG やタクティカルゲームにおいて、スキルの「射程(レンジ)」や「効果範囲(AOE: Area of Effect)」の実装は非常に複雑になりがちです。単体攻撃、直線、周囲、円形、さらには自分自身を対象とするものまで、そのバリエーションは多岐にわたります。 これらを string 型の ID や、大量の if 文、あるいは複雑なクラス継承で管理しようとすると、いつか必ず「新しいレンジを追加したのに、判定処理を書き忘れた」あるいは「このスキルタイプには不要なはずのパラメータが混入している」といったバグに直面します。 TypeScript の Union 型(特に Discriminated Union / 判別可能な共用体) を活用すれば、こうしたロジックをコンパイルレベルで安全に保護し、設計意図をコードに焼き付けることができます。本記事では、架空のゲームを題材に、any や as を一切使わずに、メンテナンス性の高いスキルシステムを構築する手法を徹底解説します。 1. Union 型でスキル種別を表現する まず、スキルの「レンジ(範囲)」を型として定義します。ここでのポイントは、単に名前を列挙するのではなく、「そのレンジを計算するために最低限必要なデータ」をセットにすることです。 なぜこの設計にするのか(データ構造の純粋性) オブジェクト指向的な発想では、BaseRange クラスを継承して AreaRange クラスを作る、といった方法が取られがちです。しかし、ゲームデータ(特に JSON などでシリアライズされるデータ)を扱う場合、純粋なオブジェクト(POJO)として扱える方がシリアリアライズの相性が良く、ロジックを分離(疎結合)しやすくなります。 // 1. 各レンジの個別定義 type MeleeRange = { type: 'melee' }; // 隣接マス(1マス) type RangedRange = { type: 'ranged'; distance: number }; // 遠距離(単体指定)。最大射程が必要。 type LineRange = { type: 'line'; length: number; width?: number; // オプションで太さを持たせることも可能 }; // 直線。貫通距離が必要。 type AreaRange = { type: 'area'; radius: number; excludeCenter?: boolean }; // 円形または四角形範囲。半径が必要。 type SurroundRange = { type: 'surround' }; // 自分の周囲8マス。パラメータ不要。 type SelfRange = { type: 'self' }; // 自分自身。パラメータ不要。 // 2. これらを統合した Union 型(Discriminated Union) export type SkillRange = | MeleeRange | RangedRange | LineRange | AreaRange | SurroundRange | SelfRange; // 3. スキルの全体定義 export interface Skill { id: string; name: string; description: string; range: SkillRange; // 型安全なレンジ定義 baseDamage: number; scalingStat: 'STR' | 'INT' | 'DEX'; // どのステータスに依存するか manaCost: number; } このように type という「タグ(判別子)」を持たせることで、TypeScript は「type が 'area' なら radius プロパティが確実に存在する」と認識します。逆に、'melee' の時には radius にアクセスしようとするとコンパイルエラーになります。これにより、不必要なデータへの依存を完全に排除できます。 ...

March 28, 2026 · 4 min