🏷️
【TypeScript】タグ付きユニオン型で型情報を正しく認識させる!
困ったこと
ユニオン型(合併型)を使って型宣言をするとき、以下のように書きたいことがあるかもしれません。
typeofを使って任意の型の場合で条件分岐させる.
/**
* typeofを使って任意の型の場合で条件分岐させる
* UserTextEvent: キーボードによって操作された時の文字情報を保持しているイベント
* UserMouseEvent: マウスの操作をした時のx軸y軸の情報を保持しているイベント
*/
type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string'){
event.value
event.target // (property) target: any 認識できない
return
}
event.value
event.target
}
しかしここで
このコードのevent.targetの型をTypeScriptが推測することができない
という問題が生じます。
handleは引数として単にUserEvent型のものを渡されているだけであるため、その要素の型の情報まで追うことはできません。
これはタグ付きユニオン型を使用することで解決することができます。
解決策
タグ付きユニオン型とは、ユニオン型で宣言する要素の型内でその型名をリテラル型のタグとして付与しておくというものです。
タグ付き合併型は、合併型の様々なケースを処理しなければならない関数を書くときに使います。たとえば、Fluxのアクション、Reduxのリデューサー、ReactのuseReducerなどを扱う場合に非常に有益です。
先ほどのコードでタグ付きユニオン型を使用した場合の具体例を見ていきましょう。
/**
* タグ付き合併型の場合
* typeofを使わず、typeというタグを持つ型で条件分岐させる
*/
// typeという要素をもち型名をタグ付けする
type UserTextEvent1 = {type: 'TextEvent', value: string, target: HTMLInputElement}
type UserMouseEvent1 = {type: 'MouseEvent', value: [number, number], target: HTMLElement}
type UserEvent1 = UserTextEvent1 | UserMouseEvent1
function handle1(event: UserEvent1) {
if (event.type === 'TextEvent'){
event.value
event.target // (property) target: HTMLInputElement 認識できる
return
}
event.value
event.target
}
良いタグはどのようなものか
- タグはユニオン型のそれぞれのケースにおいて同じ場所に存在し、オブジェクトにおいて同名(タプルの場合は同インデックス)で宣言されていること。
よくない例
// よくない例: typeの位置がUserTextEvent.typeInfo.typeとUserMouseEvent.typeと異なる
type UserTextEvent = {
typeInfo: {
type: 'TextEvent',
}
// ...
}
type UserMouseEvent = {
type: 'MouseEvent',
// ...
}
- リテラル型で宣言されていること。(stringまたは一つの型で宣言されていることが望ましい)
- タグがジェネリックでないこと。
- タグ同士が互いに排他であること。(すなわち、ユニオン型の中で一意であること)
参考文献
「プログラミングTypeScript」スケールするJavaScript アプリケーション開発
Boris Cherny 著、今村 謙士 監訳、原 隆文 訳(オライリー・ジャパン 2020年03月)
Discussion