はじめに
複数のサブモジュールを処理するモジュールに対してメッセージを投げるとき、そのメッセージがどのサブモジュールに対するものかを、Typescriptの型により判別する方法を共有します。
Chrome拡張機能を作ったときに必要なったので、誰かのお役に立てれば幸いです。
課題
以下のような階層のモジュール構成のとき:
- background
- store
- counter
- memos
- store
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型を定義する方法を共有しました。
まぁ、特別なことはしていないですが、汎用的に使えそうなので紹介してみました。
もっと良い方法があるようであれば教えて頂けると幸いです。