JavaScript | データ型

javascrpit JavaScript
スポンサーリンク

JavaScript型まわりの「よくあるバグTOP10」+デバッグ方法

① NaN が自分自身と等しくない

バグ例

const x = NaN;
if (x === NaN) console.log("NaNです"); // 実行されない

原因

  • NaN(Not a Number)は 自分自身とも等しくない という仕様。

解決策

Number.isNaN(x);     // true
Object.is(x, NaN);   // true

デバッグヒント: コンソールで Number.isNaN(value) を使う習慣をつける。

② null と undefined の混同

バグ例

let user = { name: null };
if (user.age == null) {
  console.log("未設定です");
}

原因

  • == nullnullundefined の両方を true にする。
  • データの初期値やAPIレスポンスが undefined の場合に混乱。

解決策

if (user.age === null) { ... }        // 意図的にnullを判定
if (user.age === undefined) { ... }   // 未定義を判定

バッグヒント: typeof user.age を確認。"undefined" なら未定義。

③ typeof null === "object" に騙される

バグ例

const x = null;
if (typeof x === "object") console.log("オブジェクト"); // true(!)

原因

  • 歴史的バグ(JavaScript初期の仕様ミス)だが後方互換のため残っている。

解決策

x === null         // null判定
Object.prototype.toString.call(x) // "[object Null]"

デバッグヒント: どんな型かわからない時は
Object.prototype.toString.call(value) を使うと確実。

④ == による意図しない型変換

バグ例

0 == "0"        // true
0 == []         // true
"" == 0         // true
false == "0"    // true

原因

  • == は暗黙的な型変換(coercion)を行うため、結果が直感と異なる。

解決策

  • 常に ===(厳密等価)を使う。
  • もしくは明示的に型変換する:
Number(x) === 0
Boolean(x) === true

デバッグヒント: 比較の前に typeof を出力してみる。

⑤ 0.1 + 0.2 !== 0.3 問題

バグ例

console.log(0.1 + 0.2 === 0.3); // false

原因

  • IEEE754 の浮動小数点表現による丸め誤差。

解決策

Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON  // true

デバッグヒント: 小数の比較には「許容誤差」を設ける。

⑥ 配列を typeof で判定してしまう

バグ例

typeof [] === "array"; // false(実際は "object")

原因

  • 配列は Object の一種。typeof では判別できない。

解決策

Array.isArray([]); // true
Object.prototype.toString.call([]); // "[object Array]"

デバッグヒント: 「オブジェクトだけど配列かも?」と思ったら Array.isArray()

⑦ プリミティブとオブジェクトの違いを誤解

バグ例

let a = 10;
let b = a;
b = 20;
console.log(a); // 10 ✅(独立)

let o1 = {x: 1};
let o2 = o1;
o2.x = 9;
console.log(o1.x); // 9 ❌(同じ参照)

原因

  • プリミティブは「値渡し」、オブジェクトは「参照渡し」。

解決策

  • オブジェクトをコピーするときは:
const copy = { ...o1 };            // 浅いコピー
const deep = structuredClone(o1);  // 深いコピー

デバッグヒント: console.log(o1 === o2) で参照が同じか確認。

⑧ BigInt と Number を混ぜて演算

バグ例

10n + 5; // TypeError: Cannot mix BigInt and other types

原因

  • BigIntNumber は別型。混ぜて演算不可。

解決策

10n + 5n; // OK
Number(10n) + 5; // OK(明示変換)

デバッグヒント: どちらかに統一する(型チェックを入れる)。

⑨ ラッパーオブジェクトの落とし穴

バグ例

const b = new Boolean(false);
if (b) console.log("実行される!"); // 実行される(!)

原因

  • new Boolean() はオブジェクト。オブジェクトは常に truthy。

解決策

const b = false; // プリミティブでOK

デバッグヒント: new String, new Number, new Boolean は避ける。

⑩ JSON.stringify() で情報が失われる

バグ例

原因

  • JSON では undefinedfunctionSymbol はシリアライズされない。

解決策

  • シリアライズ前に不要データを除外する、または別形式で保存。
JSON.stringify(data, (key, value) =>
  typeof value === "undefined" ? null : value
);

デバッグヒント: stringify結果を一度コンソールで確認。

追加Tips:デバッグに使える便利ワザ

目的コード例
型を厳密に調べたいObject.prototype.toString.call(x)
値の種類を判別typeof x
配列か確認Array.isArray(x)
値がNaNかNumber.isNaN(x)
nullかundefinedかx == null(両方true)
参照が同一かObject.is(a, b)
型を明示変換String(), Number(), Boolean()

まとめ(開発時チェックリスト)

比較には常に === を使う
typeof null に注意
NaN判定は Number.isNaN
配列は Array.isArray で判定
オブジェクトコピー時は参照共有に注意
BigIntとNumberは混ぜない
JSON.stringify() は失われる情報がある

型バグ発見チェックツール(Node.js 用)

以下のスクリプトを typeBugChecker.js として保存し、
node typeBugChecker.js yourfile.js で使えます。

#!/usr/bin/env node
// typeBugChecker.js
// ============================
// JavaScript 型バグ発見チェックツール
// ============================

import fs from "fs";

const code = process.argv[2];
if (!code) {
  console.log("使用方法: node typeBugChecker.js <filename.js>");
  process.exit(1);
}

const content = fs.readFileSync(code, "utf-8");

// チェックルール
const rules = [
  {
    id: "NaN-equality",
    regex: /==\s*NaN|NaN\s*==|===\s*NaN|NaN\s*===/g,
    message: "❌ NaN との比較は常に false。Number.isNaN(x) を使用してください。",
  },
  {
    id: "loose-equality",
    regex: /[^=!]==[^=]/g,
    message: "⚠️ '==' は暗黙の型変換を行います。'===' を使用してください。",
  },
  {
    id: "typeof-null",
    regex: /typeof\s+[^\s]+\s*===?\s*["']object["']/g,
    message: "⚠️ typeof null は 'object'。null 判定には `x === null` を使いましょう。",
  },
  {
    id: "typeof-array",
    regex: /typeof\s+[^\s]+\s*===?\s*["']array["']/g,
    message: "❌ typeof [] は 'object'。配列判定には Array.isArray(x) を使用してください。",
  },
  {
    id: "new-boolean",
    regex: /new\s+(Boolean|String|Number)\s*\(/g,
    message: "⚠️ ラッパーオブジェクトは避けましょう。プリミティブ型で十分です。",
  },
  {
    id: "json-stringify",
    regex: /JSON\.stringify\s*\([^)]*\)/g,
    message: "⚠️ JSON.stringify() は undefined, Symbol, 関数をシリアライズしません。",
  },
  {
    id: "null-undefined-eq",
    regex: /==\s*null|null\s*==/g,
    message: "⚠️ null == undefined は true。厳密比較 '===' を検討してください。",
  },
  {
    id: "bigint-mix",
    regex: /\d+n\s*\+\s*\d+|\d+\s*\+\s*\d+n/g,
    message: "❌ BigInt と Number を混ぜて演算しています。",
  },
  {
    id: "float-compare",
    regex: /\d+\.\d+\s*===\s*\d+\.\d+/g,
    message: "⚠️ 浮動小数点の直接比較は誤差を含む可能性があります。",
  },
  {
    id: "assign-ref",
    regex: /const\s+\w+\s*=\s*\w+\s*;/g,
    message: "⚠️ オブジェクト参照のコピーに注意。{...obj} で浅いコピーできます。",
  },
];

// 実行
let found = 0;
rules.forEach(rule => {
  const matches = content.match(rule.regex);
  if (matches) {
    found += matches.length;
    console.log(`\n${rule.message}`);
    matches.forEach(m => console.log(`  → 発見: ${m.trim()}`));
  }
});

if (found === 0) {
  console.log("✅ 型関連の潜在的バグは見つかりませんでした!");
} else {
  console.log(`\n⚠️ 合計 ${found} 件の警告が見つかりました。`);
}

使い方

① 例としてテストファイルを作成

if (typeof null === "object") console.log("null is object?");
if (0 == "0") console.log("loose eq");
let x = NaN;
if (x === NaN) console.log("NaNです");
const b = new Boolean(false);
console.log(JSON.stringify({ a: undefined }));
JavaScript

② チェック実行

node typeBugChecker.js sample.js
Bash

③ 結果例

⚠️ '==' は暗黙の型変換を行います。'===' を使用してください。
  → 発見: 0 == "0"

NaN との比較は常に false。Number.isNaN(x) を使用してください。
  → 発見: x === NaN

⚠️ typeof null'object'null 判定には `x === null` を使いましょう。
  → 発見: typeof null === "object"

⚠️ ラッパーオブジェクトは避けましょう。プリミティブ型で十分です。
  → 発見: new Boolean(false)

⚠️ JSON.stringify() は undefined, Symbol, 関数をシリアライズしません。
  → 発見: JSON.stringify({ a: undefined })

⚠️ 合計 5 件の警告が見つかりました。

拡張アイデア

  • ESLint プラグインとして統合可能
  • ルールを外部 JSON で管理
  • VSCode の保存時自動チェックに対応可
  • Webブラウザ版(<textarea>に貼ってチェック)も簡単に作れる

ブラウザで動くGUI版(React+Tailwind)

以下のコードをそのままブラウザで動かせます(Next.js や Vite React でもOK)。
ファイル名例:TypeBugChecker.jsx

import React, { useState } from "react";

export default function TypeBugChecker() {
  const [code, setCode] = useState("");
  const [results, setResults] = useState([]);

  const rules = [
    {
      id: "NaN-equality",
      regex: /==\s*NaN|NaN\s*==|===\s*NaN|NaN\s*===/g,
      message: "❌ NaNとの比較は常にfalse。Number.isNaN(x)を使いましょう。",
    },
    {
      id: "loose-equality",
      regex: /[^=!]==[^=]/g,
      message: "⚠️ '==' は暗黙の型変換を行います。'===' を使用してください。",
    },
    {
      id: "typeof-null",
      regex: /typeof\s+[^\s]+\s*===?\s*["']object["']/g,
      message: "⚠️ typeof null は 'object'。null判定には x === null を使用。",
    },
    {
      id: "typeof-array",
      regex: /typeof\s+[^\s]+\s*===?\s*["']array["']/g,
      message: "❌ typeof [] は 'object'。Array.isArray(x) を使用。",
    },
    {
      id: "new-boolean",
      regex: /new\s+(Boolean|String|Number)\s*\(/g,
      message: "⚠️ ラッパーオブジェクトは避けて、プリミティブ型を使いましょう。",
    },
    {
      id: "json-stringify",
      regex: /JSON\.stringify\s*\([^)]*\)/g,
      message: "⚠️ JSON.stringify() は undefined, Symbol, 関数をシリアライズしません。",
    },
    {
      id: "null-undefined-eq",
      regex: /==\s*null|null\s*==/g,
      message: "⚠️ null == undefined は true。厳密比較 '===' を検討。",
    },
    {
      id: "bigint-mix",
      regex: /\d+n\s*\+\s*\d+|\d+\s*\+\s*\d+n/g,
      message: "❌ BigInt と Number を混ぜて演算しています。",
    },
    {
      id: "float-compare",
      regex: /\d+\.\d+\s*===\s*\d+\.\d+/g,
      message: "⚠️ 浮動小数点の直接比較は誤差を含む可能性があります。",
    },
  ];

  const checkCode = () => {
    const warnings = [];
    rules.forEach((rule) => {
      const matches = code.match(rule.regex);
      if (matches) {
        matches.forEach((m) => {
          warnings.push({ message: rule.message, match: m.trim() });
        });
      }
    });
    setResults(warnings);
  };

  return (
    <div className="min-h-screen bg-gray-50 p-6">
      <div className="max-w-3xl mx-auto bg-white rounded-2xl shadow-md p-6 space-y-4">
        <h1 className="text-2xl font-bold text-gray-800 mb-2">
          🧠 JavaScript 型バグ発見ツール
        </h1>
        <textarea
          className="w-full h-64 border rounded-xl p-3 font-mono text-sm bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-400"
          placeholder="ここにJavaScriptコードを貼り付けてください..."
          value={code}
          onChange={(e) => setCode(e.target.value)}
        />
        <button
          onClick={checkCode}
          className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-xl font-semibold transition"
        >
          🔍 チェックする
        </button>

        <div className="mt-6">
          <h2 className="text-xl font-semibold mb-2 text-gray-700">
            結果:
          </h2>
          {results.length === 0 ? (
            <p className="text-green-600 font-medium">
              ✅ 型関連の潜在的バグは見つかりませんでした!
            </p>
          ) : (
            <ul className="space-y-2">
              {results.map((r, i) => (
                <li
                  key={i}
                  className="bg-red-50 border border-red-200 p-3 rounded-xl text-sm"
                >
                  <p className="font-semibold text-red-700">{r.message}</p>
                  <p className="text-gray-700 mt-1">→ 検出箇所: <code className="bg-gray-200 px-1 rounded">{r.match}</code></p>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </div>
  );
}
JSX

動かし方(2通り)

方法1:Vite or Next.js プロジェクトに追加

  1. React プロジェクトを用意(例:npx create-react-appnpm create vite@latest
  2. 上記コードを TypeBugChecker.jsx に保存
  3. App コンポーネントで <TypeBugChecker /> を表示
import TypeBugChecker from "./TypeBugChecker";

function App() {
  return <TypeBugChecker />;
}

export default App;
JSX

方法2:HTML単体でも動作させたい場合

以下のHTMLを index.html に保存し、ブラウザで開くだけ👇
(純粋なクライアントサイド版、React不要)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>JavaScript 型バグ発見ツール</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 p-6">
  <div class="max-w-3xl mx-auto bg-white rounded-2xl shadow-md p-6 space-y-4">
    <h1 class="text-2xl font-bold text-gray-800 mb-2">🧠 JavaScript 型バグ発見ツール</h1>
    <textarea id="code" class="w-full h-64 border rounded-xl p-3 font-mono text-sm bg-gray-100"></textarea>
    <button id="checkBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-xl font-semibold transition">🔍 チェックする</button>
    <div id="results" class="mt-6"></div>
  </div>

<script>
const rules = [
  { regex: /==\s*NaN|NaN\s*==|===\s*NaN|NaN\s*===/g, msg: "❌ NaNとの比較は常にfalse。Number.isNaN(x)を使いましょう。" },
  { regex: /[^=!]==[^=]/g, msg: "⚠️ '==' は暗黙の型変換を行います。'===' を使用してください。" },
  { regex: /typeof\s+[^\s]+\s*===?\s*["']object["']/g, msg: "⚠️ typeof null は 'object'。null判定には x === null を使用。" },
  { regex: /typeof\s+[^\s]+\s*===?\s*["']array["']/g, msg: "❌ typeof [] は 'object'。Array.isArray(x) を使用。" },
  { regex: /new\s+(Boolean|String|Number)\s*\(/g, msg: "⚠️ ラッパーオブジェクトは避けましょう。プリミティブ型で十分です。" },
  { regex: /JSON\.stringify\s*\([^)]*\)/g, msg: "⚠️ JSON.stringify() は undefined, Symbol, 関数をシリアライズしません。" },
  { regex: /==\s*null|null\s*==/g, msg: "⚠️ null == undefined は true。厳密比較 '===' を検討。" },
  { regex: /\d+n\s*\+\s*\d+|\d+\s*\+\s*\d+n/g, msg: "❌ BigInt と Number を混ぜて演算しています。" },
  { regex: /\d+\.\d+\s*===\s*\d+\.\d+/g, msg: "⚠️ 浮動小数点の直接比較は誤差を含む可能性があります。" },
];

document.getElementById("checkBtn").addEventListener("click", () => {
  const code = document.getElementById("code").value;
  const results = document.getElementById("results");
  results.innerHTML = "";

  const warnings = [];
  rules.forEach(r => {
    const matches = code.match(r.regex);
    if (matches) {
      matches.forEach(m => warnings.push({ msg: r.msg, match: m.trim() }));
    }
  });

  if (warnings.length === 0) {
    results.innerHTML = `<p class="text-green-600 font-medium">✅ 型関連の潜在的バグは見つかりませんでした!</p>`;
  } else {
    results.innerHTML = warnings.map(w =>
      `<div class="bg-red-50 border border-red-200 p-3 rounded-xl text-sm mb-2">
        <p class="font-semibold text-red-700">${w.msg}</p>
        <p class="text-gray-700 mt-1">→ 検出箇所: <code class="bg-gray-200 px-1 rounded">${w.match}</code></p>
      </div>`
    ).join("");
  }
});
</script>
</body>
</html>
HTML

機能まとめ

✅ NaN 比較
✅ typeof null / array
✅ 暗黙型変換 (==)
✅ ラッパーオブジェクト
✅ JSON.stringify の落とし穴
✅ BigInt混在演算
✅ 浮動小数点比較


ESLintルール版

このカスタムルールを使うと、プロジェクト全体で
NaN比較・暗黙型変換・typeof誤用・null/undefined混同などを自動検出できます。

ディレクトリ構成

my-eslint-plugin/
├── package.json
├── index.js
└── rules/
    ├── no-nan-compare.js
    ├── no-loose-eq.js
    ├── no-typeof-null.js
    ├── no-typeof-array.js
    ├── no-new-wrapper.js
    ├── no-bigint-mix.js
PLSQL

package.json

{
  "name": "eslint-plugin-type-safety",
  "version": "1.0.0",
  "description": "ESLint rules to detect common JavaScript type-related bugs",
  "main": "index.js",
  "keywords": ["eslint", "plugin", "type", "safety"],
  "type": "module"
}
JSON

index.js

import noNanCompare from "./rules/no-nan-compare.js";
import noLooseEq from "./rules/no-loose-eq.js";
import noTypeofNull from "./rules/no-typeof-null.js";
import noTypeofArray from "./rules/no-typeof-array.js";
import noNewWrapper from "./rules/no-new-wrapper.js";
import noBigintMix from "./rules/no-bigint-mix.js";

export const rules = {
  "no-nan-compare": noNanCompare,
  "no-loose-eq": noLooseEq,
  "no-typeof-null": noTypeofNull,
  "no-typeof-array": noTypeofArray,
  "no-new-wrapper": noNewWrapper,
  "no-bigint-mix": noBigintMix,
};

export const configs = {
  recommended: {
    rules: {
      "type-safety/no-nan-compare": "error",
      "type-safety/no-loose-eq": "warn",
      "type-safety/no-typeof-null": "warn",
      "type-safety/no-typeof-array": "warn",
      "type-safety/no-new-wrapper": "warn",
      "type-safety/no-bigint-mix": "error"
    }
  }
};
JavaScript

各ルール実装例

rules/no-nan-compare.js

export default {
  meta: {
    type: "problem",
    docs: {
      description: "NaNとの比較は常にfalse。Number.isNaNを使用すべき。",
    },
    messages: {
      avoidNaNCompare: "NaNとの比較は常にfalse。Number.isNaN(x)を使いましょう。",
    },
  },
  create(context) {
    return {
      BinaryExpression(node) {
        if (
          (node.left.name === "NaN" || node.right.name === "NaN") &&
          ["==", "==="].includes(node.operator)
        ) {
          context.report({ node, messageId: "avoidNaNCompare" });
        }
      },
    };
  },
};
JavaScript

rules/no-loose-eq.js

export default {
  meta: {
    type: "suggestion",
    docs: {
      description: "== / != は暗黙の型変換を行うため非推奨。",
    },
    messages: {
      noLooseEq: "暗黙の型変換を避けるため '===' または '!==' を使用してください。",
    },
  },
  create(context) {
    return {
      BinaryExpression(node) {
        if (node.operator === "==" || node.operator === "!=") {
          context.report({ node, messageId: "noLooseEq" });
        }
      },
    };
  },
};
JavaScript

rules/no-typeof-null.js

export default {
  meta: {
    type: "problem",
    docs: {
      description: "typeof null は 'object' を返すため注意が必要。",
    },
    messages: {
      typeofNull: "typeof null は 'object'。null 判定には x === null を使用してください。",
    },
  },
  create(context) {
    return {
      UnaryExpression(node) {
        if (
          node.operator === "typeof" &&
          node.argument.name === "null"
        ) {
          context.report({ node, messageId: "typeofNull" });
        }
      },
    };
  },
};
JavaScript

rules/no-typeof-array.js

export default {
  meta: {
    type: "problem",
    docs: {
      description: "typeof [] は 'object'。配列判定には Array.isArray(x) を使用。",
    },
    messages: {
      typeofArray: "配列判定に typeof は使えません。Array.isArray(x) を使用してください。",
    },
  },
  create(context) {
    return {
      BinaryExpression(node) {
        if (
          node.left.type === "UnaryExpression" &&
          node.left.operator === "typeof" &&
          /["']array["']/.test(node.right.raw)
        ) {
          context.report({ node, messageId: "typeofArray" });
        }
      },
    };
  },
};
JavaScript

rules/no-new-wrapper.js

export default {
  meta: {
    type: "suggestion",
    docs: {
      description: "new Boolean/String/Number は避けるべき。",
    },
    messages: {
      wrapperWarn: "ラッパーオブジェクトは避けましょう。プリミティブ型を使用してください。",
    },
  },
  create(context) {
    return {
      NewExpression(node) {
        const name = node.callee.name;
        if (["Boolean", "String", "Number"].includes(name)) {
          context.report({ node, messageId: "wrapperWarn" });
        }
      },
    };
  },
};
JavaScript

rules/no-bigint-mix.js

export default {
  meta: {
    type: "problem",
    docs: {
      description: "BigInt と Number の混在演算を禁止。",
    },
    messages: {
      bigintMix: "BigInt と Number は混在できません。どちらかに統一してください。",
    },
  },
  create(context) {
    return {
      BinaryExpression(node) {
        if (
          node.left.raw?.endsWith("n") && /^[0-9]+$/.test(node.right.raw) ||
          node.right.raw?.endsWith("n") && /^[0-9]+$/.test(node.left.raw)
        ) {
          context.report({ node, messageId: "bigintMix" });
        }
      },
    };
  },
};
JavaScript

ESLint設定で使う

プロジェクトの .eslintrc.json に以下を追加

{
  "plugins": ["type-safety"],
  "extends": ["plugin:type-safety/recommended"]
}
JSON

または特定のルールだけ個別指定:

{
  "plugins": ["type-safety"],
  "rules": {
    "type-safety/no-nan-compare": "error",
    "type-safety/no-loose-eq": "warn"
  }
}
JSON

動作確認

npm install eslint
npx eslint --init
# プラグインパスを指定して実行
npx eslint --plugin type-safety ./src
Bash

拡張アイデア

機能内容
no-json-lossJSON.stringify()で失われるキー(undefined, Symbol, function)を警告
no-float-eq小数点の厳密比較を警告
no-implicit-undefined関数戻り値がundefinedの可能性を検出
prefer-explicit-cast暗黙の型変換より明示的キャストを推奨

まとめ

対応バグ対応ルール名
NaN比較no-nan-compare
== / !=no-loose-eq
typeof nullno-typeof-null
typeof arrayno-typeof-array
new Booleanなどno-new-wrapper
BigInt混合演算no-bigint-mix
タイトルとURLをコピーしました