1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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

課題

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

  • background
    • store
      • counter
      • memos

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

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

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

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
  }
}

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

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

型生成用ジェネリック型

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

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

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型を定義します:

// 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

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

// 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が不足しているのをコンパイラでチェックするための機構です:

  default: {
    const dummy: never = store

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

まとめ

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?