なるべく型安全にvue-routerのqueryを格納する

4 min read読了の目安(約3900字

はじめに

vue-routerでは$route.queryからURLクエリパラメーターが取得できます。

import Vue from 'vue'

class Hoge extends Vue {
  created() {
    const query = this.$route.query // Dictionary<string | (string | null)[]>
  }
}

このとき$route.queryDictionary<string | (string | null)[]>という型になっています。Dictionaryというのはvue-routerの用意している型で、いわゆる連想配列のことです。
実際のところ各キーに対して文字列か配列どちらかだけを値として許したいケースが多いかと思いますので、このままの型では使い勝手があまりよろしくありません。
そこで値を取得するときに適宜型キャストを行うわけですが、これをなるべく型安全に行う方法をパターン別にいくつか考えてみたいと思います。
以下のコードはtypescript v4.3.2で型チェックを行っています。古いTSでは型推論がうまくいかない可能性があるのでご了承ください。

例外処理について

値を取得する時、要件に応じて例外処理としていくつかパターンが考えられるかと思います。
例えば

  1. 無効な値が来た場合undefinedを格納してそのまま進める
  2. デフォルト値を用意して無効な値が来た場合はデフォルト値を格納する
  3. 無効な値が来た場合はエラーをはいて処理をやめる

などでしょうか。
今回はあまり複雑な例外処理は考えず、上記のパターンだけ考慮することにします。

実装例

文字列として取得する

undefを許容して配列が来た場合一つ目を取得する

import Vue from 'vue';
interface Params {
  term?: string;
}

function queryToString(value: string | (string | null)[] | undefined): string | undefined {
  return Array.isArray(value) ? value[0] || undefined : value;
}

class Hoge extends Vue {
  params: Params = {};

  created() {
    this.getURLQuery();
  }

  getURLQuery() {
    const query = this.$route.query;
    this.params = {
      term: queryToString(query.term),
    };
  }
}

一番シンプルなパターンかと思います

udefを許さずデフォルト値を代入する

以下型変換のみ書き出します

function queryToString(value: string | (string | null)[] | undefined, defaultValue: string): string {
  return Array.isArray(value) ? value[0] || defaultValue : value || defaultValue;
}

文字列が来た時のみ許容、それ以外はエラーを返す

function queryToString(value: string | (string | null)[] | undefined): string {
  if (typeof value === 'string' && value) {
    return value;
  }
  throw new Error('Invalid query value');
}

これ以外にも数値として、配列として格納する場合も同じ要領で変換することができます。

Enumとして取得する

Enumに含まれるかどうかはObject.valuesを使って調べることができます

Enumに該当する文字列が来なかった場合はundefを返す

import Vue from 'vue';

enum Genre {
  rock = 'rock',
  jazz = 'jazz',
  progress = 'progress'
}

interface Params {
  genre?: Genre;
}

function queryToEnum<T extends string>(value: string | (string | null)[] | undefined, enumObject: Record<string, T>): T | undefined {
  const temp = Array.isArray(value) ? value[0] : value;
  if ('string' === typeof temp) {
    if (Object.values(enumObject).find((el): boolean => el === temp) !== undefined) {
      return temp as T;
    }
  }
  return undefined; // デフォルト値やエラーを返す場合はここを変える
}

class Hoge extends Vue {
  params: Params = {};

  created() {
    this.getURLQuery();
  }

  getURLQuery() {
    const query = this.$route.query;
    this.params = {
      genre: queryToEnum(query.genre, Genre)
    };
  }
}

Enumの配列で取得する

複数選択のチェックボックスでの絞り込み条件などを格納する場合です。
このコードは

  • Enumに含まれない値は破棄する
  • 重複した値は除外する
  • 該当するものがない場合は空の配列を格納する

という要件での例になります

import Vue from 'vue';

enum Genre {
  rock = 'rock',
  jazz = 'jazz',
  progress = 'progress'
}

interface Params {
  genre: Genre[];
}

function queryToEnumArray<T extends string>(value: string | (string | null)[] | undefined, enumObject: Record<string, T>): T[] {
  const enumArray = Object.values(enumObject)
  if (Array.isArray(value)) {
    const result: T[] = [];
    value.forEach((item): void => {
      const test = enumArray.find((el): boolean => { return el === item; });
      if (test && !result.includes(test)) result.push(test);
    });
    return result;
  } else {
    const test = enumArray.find((el): boolean => { return el === value; });
    if (test) return [test];
    return [];
  }
}
class Hoge extends Vue {
  params: Params = {
    genre: []
  };

  created() {
    this.getURLQuery();
  }

  getURLQuery() {
    const query = this.$route.query;
    this.params = {
      genre: queryToEnumArray(query.genre, Genre)
    };
  }
}

まとめ

場所によって要件が細かく変わってくる場合はいろいろと面倒ですが、プロジェクト全体でざっくり要件の方向性が揃えられるようであれば各々変換用のメソッドを用意して使うようにすることで、そこまで工数を増やさずに取り廻せると思います。