Typescriptでメッセージ処理モジュール分岐用の入れ子構造を定義する

# はじめに

複数のサブモジュールを処理するモジュールに対してメッセージを投げるとき、そのメッセージがどのサブモジュールに対するものかを、Typescriptの型により判別する方法を共有します。
Chrome拡張機能を作ったときに必要なったので、誰かのお役に立てれば幸いです。

https://qiita.com/taturou/items/f3e594716d174ee9bada

# 課題

以下のような階層のモジュール構成のとき:

* background
    * store
        * counter
        * memos

counterに対して処理要求のメッセージを投げたい:

```ts
const message: topMessageType = {
  type: 'background',
  background: {
    type: 'store',
    store: {
      type: 'counter',
      counter: { ... }
    }
  }
}
```

そして、以下のように階層を下ってメッセージを処理したい:

```ts
function messageDispatcher (message: topMessageType) {
  // メッセージタイプで分岐
  switch(message.type) {
  case 'background': {
    // message.type === 'background' のときは message.background が定義される
    const background = message.background
    // メッセージタイプで分岐
    switch(background.type) {
    case 'store': {
      // background.type === 'store' のときは background.store が定義される
      const store = background.store
      // メッセージタイプで分岐
      switch(store.type) {
      case 'counter': {
        // store.type === 'counter' のときは store.counter が定義される
        const counter = store.counter
        counterOperations(counter)
        break
      }
      case 'memos': {
        // store.type === 'memos' のときは store.memos が定義される
        const memos = store.memos
        counterOperations(memos)
        break
      }
      default: break
      }
    }
    default: break
    }
  }
  default: break
  }
}
```

そのためのメッセージ型を定義したい。
以下のようになるでしょう:

```ts
type topMessageType = {
  type: 'background',
  background: {
    type: 'store':
    store: {
      type: 'counter',
      counter: { ... }
    }
    | {
      type: 'memos',
      memos: { ... }
    }
  }
}
```

# 型生成用ジェネリック型

メッセージの型を手動で定義しても良いのですが、こういう構造を作るためのジェネリック型を定義しました。

https://github.com/taturou/vue3-typescript-chrome-extension/blob/afe058847ddb757e36969ccaff1fecd84fd4275e/src/background/message/_types.ts#L60

コアな部分だけ抜粋します:

```ts
type messageTypeType<T extends string> = {
  type: `${T}`
}

type messageDataWrapperType<T extends string, D> = {
  [P in T]: D
}

export type messageType<T extends string, D> = messageTypeType<T> & messageDataWrapperType<T, D>
```

## 使い方

`messageType<T>`を用いて`topMessageType`型を定義します:

```ts
// counterモジュール向けメッセージ型
type counterMessageType = messageType<'counter', {...}>

// memosモジュール向けメッセージ型
type memosMessageType = messageType<'memos', {...}>

// storeモジュール向けメッセージ型
type storeMessageDataType = memosMessageType | counterMessageType
type storeMessageType = messageType<'store', storeMessageDataType>

// backgroundモジュール向けメッセージ型
type backgroundMessageDataType = storeMessageType
type backgroundMessageType = messageType<'background', backgroundMessageDataType>

// メッセージ型
type topMessageType = backgroundMessageType
```

なんか、ムダに型が増えているだけのような気がしますが…。
各階層にメッセージ型を定義することにより、メッセージを分岐させる処理を以下のようにモジュール分割できるようになります。

```ts
// message.ts
function messageDispatcher (message: topMessageType) {
  // メッセージタイプで分岐
  switch(message.type) {
  case 'background': {
    // message.type === 'background' のときは message.background が定義される
    backgroundDispatcher(message.background)
    break
  }
  default: {
    const dummy: never = message
    break
  }
}

// background.ts
function backgroundDispatcher (background: backgroundMessageDataType) {
  // メッセージタイプで分岐
  switch(background.type) {
  case 'store': {
    // background.type === 'store' のときは background.store が定義される
    storeDispatcher(background.store)
    break
  }
  default: {
    const dummy: never = background
    break
  }
}

// store.ts
function storeDispatcher (store: storeMessageDataType) {
  // メッセージタイプで分岐
  switch(store.type) {
  case 'counter': {
    // store.type === 'counter' のときは store.counter が定義される
    counterOperations(store.counter)
    break
  }
  case 'memos': {
    // store.type === 'memos' のときは store.memos が定義される
    counterOperations(store.memos)
    break
  }
  default: {
    const dummy: never = store
    break
  }
}
```

なお、`default`の処理ですが、`case`が不足しているのをコンパイラでチェックするための機構です:

```ts
  default: {
    const dummy: never = store
```

`never`型の変数に値を代入しています。
`never`型の値は存在しないはずの値なので、いかなる値も代入できません。
そのため、この処理が **実行されうる場合** コンパイルエラーが発生します。
`case`が取りうる値を全て実装している場合、`default`には入らないため、この処理は実行されません。
しかし、`case`が不足している場合、`default`に入る可能性があるため、ここでコンパイルエラーが発生します。
つまり、全ての`case`を実装していることを、コンパイラによりチェックできます。便利！

# まとめ

サブモジュールに対するメッセージを作成する・識別するためのTypescript型を定義する方法を共有しました。
まぁ、特別なことはしていないですが、汎用的に使えそうなので紹介してみました。
もっと良い方法があるようであれば教えて頂けると幸いです。