こんにちは、とまだです。
TypeScript アドベントカレンダー 2024 のうち、1 日目の記事をお届けします!
突然ですが、みなさんは猫は好きですか?
私は猫を飼っていて、毎日のように猫に癒されています。
今回は、そんな猫の 1 日を通じて、TypeScript の型システムを理解していきましょう 🐱
1. インターフェース:朝ごはんを待つタマ
今回は「タマ」という名前の猫を例にしてみましょう。
朝 7 時。タマが起き出して、朝ごはんをねだってきました。
「にゃ〜」(ごはん!)
「にゃーん」(早くー!)
「にゃ?」(なんで起きないの?)
このように、タマには決まった情報とその時々で変わる情報があります。
TypeScript では、このような「あるものが持つべき情報の形」を interface(インターフェース) で表現します。
interface Cat {
// 決まった情報
name: string; // 名前
age: number; // 年齢
color: string; // 毛色
// 変わる情報
isHungry: boolean; // お腹が空いているか
energy: number; // 元気度(0-100)
lastMeal: Date; // 最後に食べた時間
}
// タマの今の状態
const momo: Cat = {
name: "タマ",
age: 3,
color: "茶トラ",
isHungry: true,
energy: 80,
lastMeal: new Date("2024-02-20T20:00:00"),
};
こうしておくと、以下のようなメリットがあります。
- タマの状態を正確に記録できる
- 必要な情報が抜けていないか確認できる
- 間違った型の情報(例:年齢を文字列で入れるなど)を防げる
たとえば、momo.age = "3歳"
とすると、TypeScript がエラーを出してくれます。
余計な情報を入れないように、タマの情報を管理できるのです。
2. Union Types:気まぐれなタマの機嫌
朝ごはんを食べ終えたタマ。さて、この後どうするでしょう?
猫って本当に気まぐれですよね。
- 「遊んで!」とおもちゃを持ってくる
- 「zzz...」と急に寝始める
- 「構って!」と膝の上に乗ってくる
- 「そっとしておいて」と距離を取る
このように、決まった選択肢の中から 1 つを選ぶような状況を、TypeScript では Union Types(合併型) で表現します。
// タマの機嫌を表すUnion Type
type CatMood = "遊びたい" | "寝たい" | "構ってほしい" | "そっとしておいて";
// タマの状態にmoodを追加
interface CatWithMood extends Cat {
currentMood: CatMood;
}
const momo: CatWithMood = {
name: "タマ",
age: 3,
color: "茶トラ",
isHungry: false,
energy: 80,
lastMeal: new Date(),
currentMood: "遊びたい", // この4つの選択肢以外は指定できない!
};
// タマの機嫌に応じた対応をする関数
function handleCatMood(mood: CatMood): string {
switch (mood) {
case "遊びたい":
return "ねこじゃらしで遊んであげよう";
case "寝たい":
return "そっとしておこう";
case "構ってほしい":
return "膝の上でなでさせてもらおう";
case "そっとしておいて":
return "距離を置こう";
}
}
Union Types を使うと、このようにいくつかのパターンを列挙して、それ以外の値を受け付けないようにできます。
momo.currentMood = "おかきを食べたい"; // 選択肢にないのでエラー
実務でも、このように「決まった選択肢」を型として定義することで、間違った値を防ぐことができます。
3. Optional Properties:予測不能なタマの行動
お昼頃、タマが突然走り回り始めました。
そういえば昨日は「カリカリ」を床にばらまいて遊んでいたような...?
このように、猫は予期せぬ行動を見せることがありますよね。
「今日だけの特別な行動」や「たまにしかしない行動」というものです。
TypeScript では、このような「あるかもしれないし、ないかもしれない」性質を Optional Properties で表現できます。
interface CatBehavior {
// 基本的な行動(必ず記録)
location: string; // 居場所
action: string; // している事
// 特別な行動(あれば記録)
specialAction?: string; // その日だけの特別な行動
}
// いつもの日のタマ
const normalDay: CatBehavior = {
location: "リビング",
action: "寝る",
};
ここまではいつもの日のタマですが、Optional Properties を使うと、予測不能な情報を柔軟に扱えます。
// 予測不能な行動を記録
const unexpectedBehavior: CatBehavior = {
location: "リビング",
action: "走り回る",
specialAction: "カリカリを床にばらまく",
};
実務でも、このように「必須の情報」と「任意の情報」を分けて記録することで、柔軟に情報を管理できます。
4. Generic Types:タマのおもちゃ箱作戦
夕方になり、リビングに散らかったタマのおもちゃを整理することにしました。
- ボールのおもちゃ箱
- ぬいぐるみの箱
- 猫じゃらしの箱
この「箱に何かを入れる」という考え方も、TypeScript で表現できます。
それが Generic Types(総称型) です。
// おもちゃの基本情報
interface Toy {
name: string; // おもちゃの名前
isBroken: boolean; // 壊れているか
boughtDate: Date; // 買った日
}
// 箱の定義(中に入れるものの型は後で決める)
interface Box<T> {
items: T[]; // 箱の中身
addItem(item: T): void; // アイテムを追加
getFavorite(): T; // お気に入りを取得
}
// ボールの定義
interface Ball extends Toy {
type: "転がる" | "光る" | "鈴付き";
}
// ぬいぐるみの定義
interface Plushie extends Toy {
animal: string;
hasCartnip: boolean; // またたび入り
}
// 猫じゃらしの定義
interface Teaser extends Toy {
material: "羽" | "毛" | "紐";
length: number; // センチメートル
}
// それぞれの箱を作る
// ボールの箱
const ballBox: Box<Ball> = {
items: [
{
name: "光るボール",
type: "光る",
isBroken: false,
boughtDate: new Date("2024-01-15"),
},
],
addItem(ball) {
this.items.push(ball);
},
getFavorite() {
return this.items[0];
},
};
// ぬいぐるみの箱
const plushieBox: Box<Plushie> = {
items: [
{
name: "ネズミさん",
animal: "ねずみ",
hasCartnip: true,
isBroken: false,
boughtDate: new Date("2023-12-25"),
},
],
addItem(plushie) {
this.items.push(plushie);
},
getFavorite() {
return this.items[0];
},
};
// 猫じゃらしの箱
const teaserBox: Box<Teaser> = {
items: [
{
name: "きらきら羽じゃらし",
material: "羽",
length: 45,
isBroken: false,
boughtDate: new Date("2024-02-01"),
},
],
addItem(teaser) {
this.items.push(teaser);
},
getFavorite() {
return this.items[0];
},
};
ちょっと複雑に見えますが、Generic Types を使うことで、同じ構造で異なる型を扱うことができます。
// 新しいボールを追加
ballBox.addItem({
name: "鈴付きボール",
type: "鈴付き",
isBroken: false,
boughtDate: new Date(),
});
// それぞれの箱からお気に入りを取得
const favoriteBall = ballBox.getFavorite(); // Ball型
const favoritePlush = plushieBox.getFavorite(); // Plushie型
const favoriteTeaser = teaserBox.getFavorite(); // Teaser型
アプリが大きくなっても、型の整合性を保ちながら柔軟に拡張できるので、実務でもよく使われる手法です。
5. Mapped Types:タマの一日を記録しよう
夜になり、今日のタマの様子を日記につけることにしました。
「朝」「昼」「夕方」「夜」、それぞれの時間でタマは何をしていたでしょう?
このように「決まった時間帯それぞれに情報を記録したい」という場合、Mapped Types(写像型) が便利です。
// 時間帯を定義
type TimeOfDay = "朝" | "昼" | "夕方" | "夜";
// 各時間帯の記録内容
interface DailyActivity {
location: string; // どこにいた?
mood: CatMood; // 機嫌は?
memo?: string; // 特記事項
}
// 一日の記録を作る
type DailySchedule = {
[Time in TimeOfDay]: DailyActivity; // 時間帯ごとの記録
};
// タマの一日
const momosDay: DailySchedule = {
朝: {
location: "キッチン",
mood: "お腹すいた",
memo: "いつもより30分早く起きた",
},
昼: {
location: "窓際",
mood: "そっとしておいて",
},
夕方: {
location: "リビング",
mood: "遊びたい",
memo: "新しいおもちゃで遊ぶ",
},
夜: {
location: "ソファー",
mood: "寝たい",
},
};
Mapped Types を使うと、このように「構造化された情報」を管理することができます。
momosDay.夜 = { location: "ベッド", mood: "寝る" }; // OK
momosDay.夜 = { location: "ベッド", mood: "" }; // エラー
```
## まとめ:タマと学んだ TypeScript の型システム
タマの一日を通じて、TypeScript の主要な型システムについて学んできました。
最後に軽く振り返ってみましょう。
1. **Interface(インターフェース)**
- タマの基本情報を定義
- 必要な情報を明確に示せる
2. **Union Types(合併型)**
- タマの気分のように、決まった選択肢を表現
- 想定外の値を防げる
3. **Optional Properties(省略可能なプロパティ)**
- 予測不能な猫の行動をうまく記録
- 必要なときだけ情報を追加できる
4. **Generic Types(総称型)**
- おもちゃ箱のような「入れ物と中身」の関係を表現
- 同じ構造で異なる型を扱える
5. **Mapped Types(写像型)**
- 一日の記録のように、構造化された情報を管理
- 必要な情報の漏れを防げる
他にもアドベントカレンダー記事を書いています!
他にも、2024 年のアドベントカレンダーに参加しています。
以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!