初心者でも安心!筋トレで学ぶTypeScript型設計の基本
はじめに
こんにちは。株式会社トリドリでバックエンドエンジニアをしている松田です!
弊社はフロントエンドもバックエンドも、TypeScriptメインで開発をすることが多く、型を使いこなせることが必要になってきます。そして経験が浅いうちには使いこなすことはおろか、理解すること自体少し労力がかかるのがこのジェネリクスです。
そこで今回はこのジェネリクスも含めた型設計の基本について、「筋トレ」を題材として理解を深めましょう!
初めは右も左もわからずジムに入会したてのトレーニー状態から、徐々に筋トレの仕方がわかってくるまでのストーリー仕立てで進めていきます。
ジェネリクス、インターフェース、継承について学ぶ
基本のジェネリクス
〜初めてのジム〜
ジェネリクスは、プログラムを柔軟かつ効率的にするためのツールです。
初めてジムに来たあなたは、何をしていいか分からず、目の前にあるダンベルを手に取り、見よう見まねで筋トレを始めます。そのトレーニングを記録していきます。
function recordTraining<T>(weight: T): T {
return weight;
}
技術的解説
この関数「型の再利用性」を実現します。
T
は「型パラメータ」と言い、ここではどんな重量にも対応する型の変数として機能します。T
という文字に特別な意味があるわけではなく、慣例的にジェネリクスで使われる記号です。
関数を呼び出す際に、TypeScriptが引数の型から T
を推論し、戻り値の型を決定します。
使用例
const trainingFirst = recordTraining(3); // 出力: 3
const trainingSecond = recordTraining("10kg"); // 出力: "10kg"
const trainingThird = recordTraining([5, 10, 15]); // 出力: [5, 10, 15]
このように、どんな重量(型)でも記録できるため、幅広いトレーニングに対応可能です。
引数を追加する
〜メニューを考慮したトレーニング〜
次に、トレーニングでは「どんな重さを扱ったか」だけではなく「どのメニューをしたのか」が重要だと気づきます。
そこで、先ほどの関数にトレーニングメニューを記録する機能を追加します。
function recordTraining<T, U>(weight: T, menu: U): { weight: T; menu: U } {
return { weight, menu };
}
技術的解説
- 複数の型パラメータ・・・ここでの
U
も、T
と同様慣例的に使われている記号です - 戻り値の型の定義として
{ weight: T; menu: U }
を指定することで関数の返り値も型安全に記述ができる
使用例
const result = recordTraining("3kg", "アームカール");
// { weight: "3kg", menu: "アームカール" }
これにより、「どのメニューでどれくらいの重さを使ったか」を明確に記録できます。
型の拡張(interface/extends)
〜セットとレップを考慮したトレーニング〜
さらにあなたは、筋トレに「レップ」「セット」という概念があることを知りました。
レップ:運動を1回繰り返すこと
セット:一定回数のレップのまとまりのこと
この概念を取り入れてみます。
interface TrainingMenu {
name: string; // 種目名
reps: number; // レップ数
sets: number; // セット数
}
function recordTraining<T, U extends TrainingMenu>(weight: T, menu: U): { weight: T; menu: U; } {
return { weight, menu };
}
技術的解説
- 型の制約 (extends)
U extends TrainingMenu
によって、menu
オブジェクトが必ずTrainingMenu
の型を持っていることが保証されています。これにより柔軟性を維持しつつ型安全性が高まっています。
使用例
const trainingMenu = { name: "アームカール", reps: 10, sets: 3 };
const result = recordTraining(3, trainingMenu);
// resultの結果
// { weight: 3, menu: { name: "アームカール", reps: 10, sets: 3 } }
さらに型を拡張
〜有酸素と無酸素運動を考慮したトレーニング〜
ダンベルのトレーニングのような無酸素運動を習得した後、さらにトレーニングの幅を広げたいと考え、ランニングマシンを試してみることにしました。
// 共通部分のインターフェース
interface BaseTraining {
name: string; // トレーニング名
notes?: string; // メモ
}
// 有酸素運動のインターフェース
interface AerobicTraining extends BaseTraining {
duration: number; // 実施時間(分)
}
// 無酸素運動のインターフェース
interface AnaerobicTraining extends BaseTraining {
reps: number; // レップ数
sets: number; // セット数
weight: number; // 重量
}
// トレーニング記録関数
function recordTraining<T extends BaseTraining>(training: T): T & { recordedAt: Date } {
return { ...training, recordedAt: new Date() }; // 記録日時を追加
}
技術的解説
- インターフェースの継承
BaseTrainingという共通プロパティを用意して、各トレーニングインターフェースにて継承しています。継承を使うことでコードの冗長性を減らしています(再利用性が向上)。 - 型拡張(
T & {}
)
T & { recordedAt: Date } によって、元の型に記録日時のプロパティを安全に追加しています。
使用例
// 有酸素運動の記録
const aerobic = { name: "ウォーキング", duration: 30, notes: "緩めのスピード" };
const recordedAerobic = recordTraining(aerobic);
console.log(recordedAerobic);
// 出力:
// {
// name: "ウォーキング",
// duration: 30,
// notes: "Easy pace",
// recordedAt: "2024-12-25T10:00:00.000Z" // 実行時の日時
// }
// 無酸素運動の記録
const anaerobic = { name: "アームカール", reps: 10, sets: 3, weight: 3, notes: "ベンチ台に座って実施" };
const recordedAnaerobic = recordTraining(anaerobic);
console.log(recordedAnaerobic);
// 出力:
// {
// name: "アームカール",
// reps: 10,
// sets: 3,
// weight: 3,
// notes: "ベンチ台に座って実施",
// recordedAt: "2024-12-25T10:00:00.000Z" // 実行時の日時
// }
まとめ
TypeScriptのジェネリクスも含めた型設計の基本について、筋トレを題材にして以下の技術的ポイントを学びました:
-
ジェネリクスの基本
- 型パラメータ T による柔軟な関数設計
- 型推論による記述の簡略化
-
複数の型パラメータ
- T と U を使用し、異なる型を組み合わせた処理の実現
-
型の制約 (extends)
- 必要な型のプロパティを保証しながら柔軟性を維持
-
型拡張と継承
- インターフェースの継承で共通部分を効率的に管理
- 型拡張 (T & {}) による安全なプロパティ追加
ジェネリクスは一見複雑に思える技術ですが、理解が深まればプログラムの柔軟性と安全性を向上させることができる便利なツールになります。皆さんもぜひ次回の開発でぜひ活用してみてください!そして筋トレもぜひ一緒に!
参考記事
Discussion