JavaScriptで“データのお掃除”!nullやundefinedにも優しい小技集

こんにちは、さるまりんです。
今回はJavaScriptで入力値や設定値を落ちずに・きれいに・揃えるための小技を集めてみました。
フォーカスしたのはnull/undefinedでも落ちない」「最低限きれいな形にそろえる」ことです。
XSS対策のHTMLサニタイズは別テーマ、ぱっと扱うには大きそうなので。ここではデータ整形に集中します。

それぞれ関数として、実際に使うどうなるのか?を書き出しています。

1) まずは“nullish-safe”の基本形

// 値が null または undefined か?
const isNil = (v) => v == null; // == を使うと null/undefined をまとめて判定

// 文字列化(nullishなら "")、トリムや余分な空白も整える
const toStringSafe = (v, { trim = true, collapse = true, normalize = true } = {}) => {
  if (isNil(v)) return "";
  let s = String(v);
  if (normalize && s.normalize) s = s.normalize("NFKC"); // 表記ゆれを整える
  if (trim) s = s.trim();
  if (collapse) s = s.replace(/\s+/g, " "); // 連続空白→1つ
  return s;
};

// 文字列トリムだけ nullish-safe に(超軽量版)
const trimSafe = (v) => (isNil(v) ? "" : String(v).trim());

💡 補足:normalize("NFKC") とは?
Unicodeの「正規化」と呼ばれる機能で、全角英数や互換文字など、見た目が同じでも異なる文字コードを統一した形に整える処理です。
例えば"Test 123""Test 123" のように、全角を半角に直してくれます。
NFKC(Compatibility Composition)は“互換文字”も含めて統一するため、入力値のばらつきを減らすのに実用的です。

試してみます

toStringSafe("  Test  123  ");   // "Test 123"(全角→半角などNFKCで整う)
toStringSafe(null);                     // ""
trimSafe(undefined);                    // ""

2) 改行・ホワイトスペースの扱い

// 改行を \n に正規化(CRLF/CR を LF に)
const normalizeNewlines = (s) => toStringSafe(s).replace(/\r\n?/g, "\n");

// 文中の“見えない制御文字”を除去(タブ・改行は許可)
const stripControlChars = (s) => toStringSafe(s).replace(
  /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,
  ""
);

// 前後の空白を削り、内部は1スペースに
const normalizeSpace = (s) => toStringSafe(s, { trim: true, collapse: true });

試してみます

normalizeNewlines("a\r\nb\rc"); // "a\nb\nc"
stripControlChars("a\u0007b");  // "ab"
normalizeSpace("  foo   bar  "); // "foo bar"

3) 数値・真偽値への安全な変換

// 数値化(失敗したら nullを返す) + 範囲指定(最小、最大)もできるように
const toNumberSafe = (v, { min = -Infinity, max = Infinity } = {}) => {
  if (isNil(v)) return null;
  const n = Number(String(v).trim());
  if (!Number.isFinite(n)) return null;
  return Math.min(max, Math.max(min, n));
};

// 整数化(基数を指定するparseIntではなくNumber→整数化で安全に)
const toIntSafe = (v, opts) => {
  const n = toNumberSafe(v, opts);
  return n == null ? null : Math.trunc(n);
};

// よくある文字列表現をtrue/falseに(未定義ならnullに)
const toBooleanSafe = (v) => {
  if (isNil(v)) return null;
  const s = String(v).trim().toLowerCase();
  if (["true", "1", "on", "yes", "y"].includes(s)) return true;
  if (["false", "0", "off", "no", "n"].includes(s)) return false;
  return null; // 判定不能
};

試してみます

toNumberSafe(" 42 ");                 // 42
toNumberSafe("9.9", { max: 5 });      // 5
toIntSafe(" 3.14 ");                   // 3
toBooleanSafe("YES");                  // true
toBooleanSafe("0");                    // false
toBooleanSafe("maybe");                // null

4) 配列・JSONなどの受け皿

// 値を配列に寄せる(nullish→空配列、配列ならシャローコピーして、それ以外は単一要素化)
const toArray = (v) => {
  if (isNil(v)) return [];
  return Array.isArray(v) ? [...v] : [v];
};

// JSONを安全に(失敗はnull)
const safeJsonParse = (v) => {
  if (isNil(v)) return null;
  try { return JSON.parse(String(v)); }
  catch { return null; }
};

試してみます

toArray(null);         // []
toArray("tag");        // ["tag"]
toArray(["a","b"]);    // ["a","b"]
safeJsonParse('{"a":1}');  // {a:1}
safeJsonParse("{oops}");   // null

5) HTML系の“整え”と注意点

// 表示用に最低限のエスケープをします(挿入時のXSS対策には不十分です)
const escapeHTML = (s) =>
  toStringSafe(s)
    .replaceAll("&", "&")
    .replaceAll("<", "<")
    .replaceAll(">", ">")
    .replaceAll('"', """)
    .replaceAll("'", "'");

// ※ 本格的に「ユーザー入力のHTMLを安全化」したい場合は専用ライブラリを使うのが良いでしょう。
//   ここではあくまで“整形ユーティリティ”として簡易表示向けの最小限の実装です。

試してみます

escapeHTML(`<img src=x onerror=alert(1)>`); // "<img src=x onerror=alert(1)>"

6) ちょっとした“型合わせ”の例(フォームを想定しています)

入力 → サニタイズ → 型整合 の流れを1つにまとめるサンプルです。

const sanitizeUserDraft = (raw) => {
  const name = normalizeSpace(raw?.name);
  const email = toStringSafe(raw?.email, { trim: true, collapse: true }).toLowerCase();
  const age = toIntSafe(raw?.age, { min: 0, max: 150 }); // 年齢の常識範囲にクランプ
  const tags = toArray(raw?.tags).map((t) => normalizeSpace(t)).filter(Boolean);
  const bio = stripControlChars(normalizeNewlines(raw?.bio));
  return { name, email, age, tags, bio };
};

試してみるとこうなります。

sanitizeUserDraft({
  name: "  さる まりん ",
  email: "  USER@EXAMPLE.COM ",
  age: "  200 ",
  tags: "js",       // 単体でも配列化される
  bio: "こんにちは\r\nさようなら\u0007"
});
// => { name: "さる まりん", email: "user@example.com", age: 150, tags: ["js"], bio: "こんにちは\nさようなら" }

7) 小さな“パイプ”で書きやすく

const pipe = (x, ...fns) => fns.reduce((v, fn) => fn(v), x);

// 例:名前を「NFKC→制御文字除去→スペース整形→トリム」で通す
const cleanName = (s) => pipe(
  s,
  (x) => toStringSafe(x, { trim: false, collapse: false, normalize: true }),
  stripControlChars,
  normalizeSpace,
  trimSafe
);

cleanName("  Salu   まりん  "); // "Salu まりん"

今回はJavaScriptでの“データ整形(お掃除)”をやってみました。XSS対策はもっとちゃんと向き合わないといけないので分けて考えています。
nullish-safe な設計で安全に使えることを考えてます。これでバグが減るはずです。
これをベースに、次はブラウザでの入力補正や、バックエンドでの型安全処理にも発展させていけそうです。

JavaScriptのエラーって見つけにくいことがあるので、初めから起こりにくく、できるところからやっていきたいと思います。

下にそのままコピーできる形でコードを残しておきます。

読んでくださってありがとうございました。
それではまた!


1ファイルにまとめて貼れる版

// ===== nullish-safe 判定
const isNil = (v) => v == null;

// ===== 文字列系
const toStringSafe = (v, { trim = true, collapse = true, normalize = true } = {}) => {
  if (isNil(v)) return "";
  let s = String(v);
  if (normalize && s.normalize) s = s.normalize("NFKC");
  if (trim) s = s.trim();
  if (collapse) s = s.replace(/\s+/g, " ");
  return s;
};
const trimSafe = (v) => (isNil(v) ? "" : String(v).trim());
const normalizeNewlines = (s) => toStringSafe(s).replace(/\r\n?/g, "\n");
const stripControlChars = (s) => toStringSafe(s).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
const normalizeSpace = (s) => toStringSafe(s, { trim: true, collapse: true });
const escapeHTML = (s) =>
  toStringSafe(s).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
                 .replaceAll('"', """).replaceAll("'", "'");

// ===== 数値・真偽値
const toNumberSafe = (v, { min = -Infinity, max = Infinity } = {}) => {
  if (isNil(v)) return null;
  const n = Number(String(v).trim());
  if (!Number.isFinite(n)) return null;
  return Math.min(max, Math.max(min, n));
};
const toIntSafe = (v, opts) => {
  const n = toNumberSafe(v, opts);
  return n == null ? null : Math.trunc(n);
};
const toBooleanSafe = (v) => {
  if (isNil(v)) return null;
  const s = String(v).trim().toLowerCase();
  if (["true", "1", "on", "yes", "y"].includes(s)) return true;
  if (["false", "0", "off", "no", "n"].includes(s)) return false;
  return null;
};

// ===== 配列・JSON
const toArray = (v) => (isNil(v) ? [] : Array.isArray(v) ? [...v] : [v]);
const safeJsonParse = (v) => {
  if (isNil(v)) return null;
  try { return JSON.parse(String(v)); } catch { return null; }
};

// ===== 小パイプ
const pipe = (x, ...fns) => fns.reduce((v, fn) => fn(v), x);

// ===== デモ
// console で: toStringSafe("  Test  ")