気まぐれな「猫」の行動で理解する TypeScript の型 🐈


こんにちは、とまだです。

TypeScript アドベントカレンダー 2024 のうち、1 日目の記事をお届けします！

https://qiita.com/advent-calendar/2024/typescript

突然ですが、みなさんは猫は好きですか？

私は猫を飼っていて、毎日のように猫に癒されています。

今回は、そんな猫の 1 日を通じて、TypeScript の型システムを理解していきましょう 🐱

## 1. インターフェース：朝ごはんを待つタマ

今回は「タマ」という名前の猫を例にしてみましょう。

朝 7 時。タマが起き出して、朝ごはんをねだってきました。

```
「にゃ〜」（ごはん！）
「にゃーん」（早くー！）
「にゃ？」（なんで起きないの？）
```

このように、タマには**決まった情報**と**その時々で変わる情報**があります。

TypeScript では、このような「あるものが持つべき情報の形」を **interface（インターフェース）** で表現します。

```typescript
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（合併型）** で表現します。

```typescript
// タマの機嫌を表す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 を使うと、このようにいくつかのパターンを列挙して、それ以外の値を受け付けないようにできます。

```typescript
momo.currentMood = 'おかきを食べたい'; // 選択肢にないのでエラー
```

実務でも、このように「決まった選択肢」を型として定義することで、間違った値を防ぐことができます。

## 3. Optional Properties：予測不能なタマの行動

お昼頃、タマが突然走り回り始めました。
そういえば昨日は「カリカリ」を床にばらまいて遊んでいたような...？

このように、猫は**予期せぬ行動**を見せることがありますよね。
「今日だけの特別な行動」や「たまにしかしない行動」というものです。

TypeScript では、このような「あるかもしれないし、ないかもしれない」性質を **Optional Properties** で表現できます。

```typescript
interface CatBehavior {
  // 基本的な行動（必ず記録）
  location: string; // 居場所
  action: string; // している事

  // 特別な行動（あれば記録）
  specialAction?: string; // その日だけの特別な行動
}

// いつもの日のタマ
const normalDay: CatBehavior = {
  location: 'リビング',
  action: '寝る',
};
```

ここまではいつもの日のタマですが、Optional Properties を使うと、予測不能な情報を柔軟に扱えます。

```typescript
// 予測不能な行動を記録
const unexpectedBehavior: CatBehavior = {
  location: 'リビング',
  action: '走り回る',
  specialAction: 'カリカリを床にばらまく',
};
```

実務でも、このように「必須の情報」と「任意の情報」を分けて記録することで、柔軟に情報を管理できます。

## 4. Generic Types：タマのおもちゃ箱作戦

夕方になり、リビングに散らかったタマのおもちゃを整理することにしました。

- ボールのおもちゃ箱
- ぬいぐるみの箱
- 猫じゃらしの箱

この「箱に何かを入れる」という考え方も、TypeScript で表現できます。

それが **Generic Types（総称型）** です。

```typescript
// おもちゃの基本情報
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 を使うことで、同じ構造で異なる型を扱うことができます。

```typescript
// 新しいボールを追加
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（写像型）** が便利です。

```typescript
// 時間帯を定義
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 を使うと、このように「構造化された情報」を管理することができます。

````typescript
momosDay.夜 = { location: "ベッド", mood: "寝る" }; // OK
momosDay.夜 = { location: "ベッド", mood: "" }; // エラー
```

## まとめ：タマと学んだ TypeScript の型システム

タマの一日を通じて、TypeScript の主要な型システムについて学んできました。

最後に軽く振り返ってみましょう。

1. **Interface（インターフェース）**

   - タマの基本情報を定義
   - 必要な情報を明確に示せる

2. **Union Types（合併型）**

   - タマの気分のように、決まった選択肢を表現
   - 想定外の値を防げる

3. **Optional Properties（省略可能なプロパティ）**

   - 予測不能な猫の行動をうまく記録
   - 必要なときだけ情報を追加できる

4. **Generic Types（総称型）**

   - おもちゃ箱のような「入れ物と中身」の関係を表現
   - 同じ構造で異なる型を扱える

5. **Mapped Types（写像型）**
   - 一日の記録のように、構造化された情報を管理
   - 必要な情報の漏れを防げる
````

## 他にもアドベントカレンダー記事を書いています！

他にも、2024 年のアドベントカレンダーに参加しています。

以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです！

https://qiita.com/tomada/items/025b3745df8c484f5856