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でUnion型の判別

Posted at

実際に案件でUnion型を判別していた処理をリファクタリングした時の内容を共有します。

もともとのコードは以下のような内容です。

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

参考

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard
https://typescript-jp.gitbook.io/deep-dive/type-system/discriminated-unions

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?