TypeScript 検索条件のURL変換をまとめて値の取り違えを防ぐ
検索画面が増えると、入力値をURLのクエリ文字列へ変換する処理も増えます。
画面ごとに変換を書くと、ある画面では空欄を送信し、別の画面では省略する、といった違いが生まれます。特に0やfalseを、未入力として落としてしまうことがあります。
今回は、APIへ検索条件を渡すルールを、小さな関数へまとめる方法を紹介します。
先に「送らない値」を決める
次のような真偽値の判定は、検索条件では注意が必要です。
値が消えてしまう判定if (value) {
// URLへ追加する
}0、false、空文字列も条件を満たさないため、省略されます。たとえば「非公開の商品」を表すpublished=falseまで消えると、検索の意味が変わります。
この記事では次のルールにします。
| 値 | URLへの変換 |
|---|---|
null、undefined | 送信しない |
0 | 0として送る |
false | falseとして送る |
| 空文字列 | 空の値として送る |
| 配列 | 同じキーを繰り返す |
空文字列も省略したい場合は、フォームの値を検索条件へ変換する段階でundefinedにします。共通関数が勝手に空欄の意味を変えないようにします。
URLSearchParamsで組み立てる
buildQueryString.tstype 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クライアントの生成ツールが変換を担当している場合は、まずその動作を確認します。独自の変換を追加して、同じ値を二度処理しないようにします。
画面ごとの変換の違いに困ったら、送る値と省略する値を表にしてから共通化すると、短いコードでも判断の根拠を残せます。