TypeScriptでUnion型の判別
実際に案件でTypeScriptのリファクタリングを行ったのでざっくりメモ。
もともとのコードは以下のような内容。
type Form = CheckForm | FreeTextForm; interface BaseForm { id: number; type: 'checkbox' | 'freeText'; description: string; } interface CheckForm extends BaseForm { options: Options[]; } interface FreeTextForm extends BaseForm { maxLength: number; } const form: Form = { id: 1, type: 'checkbox', description: 'select only one', options: [{key: 1,value: '1'},{key: 2,value: '2'}] } function findOptionIndex(form, key) { // CheckFormのときだけ処理をしたい if (form.type === 'checkbox') { const f = form as CheckForm; // form.optionsを参照できるように型を指定している return form.options.findIndex((opt) => opt.key === key); } else { return -1; } }
Formは複数の型を結合したUnion型のため、form.options
を参照しようとするとoptions
をメンバとして持っていない可能性があると怒られてしまう。
そのためas
を使ってform
の型をCheckForm
として扱うように指定されていた。
しかしこの記述をするにはいちいち変数にオブジェクトを入れ直さないといけないのであまりよろしくない。 なので、まず特定の条件をクリアした場合にどういう型として扱うか指定することができるユーザー定義の型ガードというので書き直してみた。
const isCheckForm = (form: Form): form is CheckForm => { return form.type === 'checkbox'; } function findOptionIndex(form: Form, key: number) { if (isCheckForm(form)) { return form.options.findIndex((opt) => opt.key === key); } else { return -1; } }
上記のように関数の返り値をarg is TypeName
とすることで、関数の返り値がtrue
だった場合に引数にいれたarg
をTypeName
で指定した型のオブジェクトとして扱うことができる。
これで可読性もだいぶ良くなり型の予測もしやすくなった。
ただ一つ問題があり、この方法だとis
の後ろにどんな型でも指定できるので、開発者が間違った型を指定するとTypeScriptがエラーを検知できなくなるリスクがある。
Union型はメンバが持つリテラル型のプロパティを元にUnion型のメンバを判別できるらしく、最終的に先ほどのリファクタリングは諦めて、CheckForm
とFreeTextForm
のtype
プロパティに文字列を指定することで解決できた。
内容は以下のようになった。
type Form = CheckForm | FreeTextForm; interface BaseForm { id: number; type: 'checkbox' | 'freeText'; description: string; } interface CheckForm extends BaseForm { type: 'checkbox'; // 判別のためにリテラル型のプロパティを含める options: Options[]; } interface FreeTextForm extends BaseForm { type: 'freeText'; // 判別のためにリテラル型のプロパティを含める maxLength: number; } const form: Form = { id: 1, type: 'checkbox', description: 'select only one', options: [{key: 1,value: '1'},{key: 2,value: '2'}] } function findOptionIndex(form: Form, key: number) { // CheckFormのときだけ処理をしたい if (form.type === 'checkbox') { // CheckFormのtypeプロパティと一致しているため、CheckFormと認識される return form.options.findIndex((opt) => opt.key === key); } else { return -1; } }