thasmto's blog

フロントエンドエンジニアやってます。プログラマーとして学んだことや考えていることを投稿するブログです。

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だった場合に引数にいれたargTypeNameで指定した型のオブジェクトとして扱うことができる。 これで可読性もだいぶ良くなり型の予測もしやすくなった。

ただ一つ問題があり、この方法だとisの後ろにどんな型でも指定できるので、開発者が間違った型を指定するとTypeScriptがエラーを検知できなくなるリスクがある。

Union型はメンバが持つリテラル型のプロパティを元にUnion型のメンバを判別できるらしく、最終的に先ほどのリファクタリングは諦めて、CheckFormFreeTextFormtypeプロパティに文字列を指定することで解決できた。 内容は以下のようになった。

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

参考