zukucode
主にWEB関連の情報を技術メモとして発信しています。

TypeScript 検索条件のURL変換をまとめて値の取り違えを防ぐ

検索画面が増えると、入力値をURLのクエリ文字列へ変換する処理も増えます。

画面ごとに変換を書くと、ある画面では空欄を送信し、別の画面では省略する、といった違いが生まれます。特に0やfalseを、未入力として落としてしまうことがあります。

今回は、APIへ検索条件を渡すルールを、小さな関数へまとめる方法を紹介します。

先に「送らない値」を決める

次のような真偽値の判定は、検索条件では注意が必要です。

値が消えてしまう判定
if (value) {
  // URLへ追加する
}

0、false、空文字列も条件を満たさないため、省略されます。たとえば「非公開の商品」を表すpublished=falseまで消えると、検索の意味が変わります。

この記事では次のルールにします。

値URLへの変換
null、undefined送信しない
00として送る
falsefalseとして送る
空文字列空の値として送る
配列同じキーを繰り返す

空文字列も省略したい場合は、フォームの値を検索条件へ変換する段階でundefinedにします。共通関数が勝手に空欄の意味を変えないようにします。

URLSearchParamsで組み立てる

buildQueryString.ts
type QueryPrimitive = string | number | boolean | null | undefined;
type QueryValue = QueryPrimitive | readonly QueryPrimitive[];

export const buildQueryString = (
  params: Record<string, QueryValue>,
): string => {
  const result = new URLSearchParams();

  for (const [key, value] of Object.entries(params)) {
    const values = Array.isArray(value) ? value : [value];

    for (const item of values) {
      if (item === null || item === undefined) continue;
      result.append(key, String(item));
    }
  }

  return result.toString();
};

appendで値を追加し、toStringでクエリ文字列を作ります。変換結果に先頭の?は含まれません。MDNのURLSearchParamsの説明でも使い方を確認できます。

手動でkey=valueをつなぐより、値に&や空白が含まれる場合も扱いやすくなります。渡す値を先にencodeURIComponentで変換すると二重に変換されるので、通常は元の値を渡します。

配列の書き方はAPIと合わせる

検索条件の例
import { buildQueryString } from './buildQueryString';

const query = buildQueryString({
  keyword: 'A&B',
  published: false,
  minPrice: 0,
  tags: ['book', 'gift'],
  category: undefined,
});

console.log(query);
// keyword=A%26B&published=false&minPrice=0&tags=book&tags=gift

この例は、tags=book&tags=giftのように同じ名前を繰り返します。

APIによっては、カンマ区切りやtags[]を要求することもあります。配列の表現に唯一の正解があるわけではないため、接続先の受け取り方と一致させます。

型にはオブジェクトや日時を含めていません。日時ならISO形式の文字列へ変換するなど、送信形式を決めてから渡します。数値もNaNなどが入らないよう、検索条件を作る側で検証します。

既存のクエリがあるURLにも注意する

url + '?' + queryだけでは、元のURLにすでに?がある場合に壊れます。URL全体を扱う場合はURLも使えます。

URLへ検索条件を追加する例
import { buildQueryString } from './buildQueryString';

const url = new URL('/api/products?sort=name', 'https://example.com');
const params = new URLSearchParams(buildQueryString({ tags: ['book', 'gift'] }));

params.forEach((value, key) => {
  url.searchParams.append(key, value);
});

console.log(url.toString());

この例は既存の値へ追加します。同じキーを置き換えたい場合は、そのキーの既存値を削除してから追加するなど、別のルールにします。

画面固有の判断と共通変換を分ける

「価格が空欄なら条件を付けない」「検索文字の前後の空白を取り除く」といった判断は、フォーム側で行います。

フォームの値を検索条件にする例
const keyword = inputKeyword.trim();
const params = {
  keyword: keyword === '' ? undefined : keyword,
};

inputKeywordは入力欄の文字列を表す変数です。この例をそのまま共通のURL変換へ組み込むと、空白を意味のある文字として扱う別の検索にも影響します。

共通化するのは、プリミティブ値を送信する方法です。各画面がどんな条件を作るかまではまとめません。

変換のテストでは境界の値を確認する

入力確認すること
{ count: 0 }count=0が残る
{ enabled: false }enabled=falseが残る
{ q: '' }この例ではq=になる
{ q: null }qを送らない
{ tags: [] }tagsを送らない
{ q: 'A&B' }読み戻すと元の文字列になる
{ tags: ['a', 'b'] }getAll('tags')で2件取り出せる

APIクライアントの生成ツールが変換を担当している場合は、まずその動作を確認します。独自の変換を追加して、同じ値を二度処理しないようにします。

APIの型と通信コードを生成して修正漏れを減らす方法

画面ごとの変換の違いに困ったら、送る値と省略する値を表にしてから共通化すると、短いコードでも判断の根拠を残せます。


関連記事