0️⃣

TypeScript/JavaScript における null と undefined 完全ガイド

14
TypeScriptReact

TypeScript/JavaScript における null と undefined 完全ガイド

1. 基礎理解編:null と undefined の違い

🎯 なぜこれを理解する必要があるのか?

TypeScript/JavaScript では「値がない」を表現する方法が 2 つあります。この違いを理解しないと:

  • 予期しないバグが発生する
  • チーム開発で混乱が生じる
  • 型安全性が損なわれる

📊 基本的な違い一覧表

項目 undefined null
意味 値が代入されていない 値の意図的な不在
発生方法 自然発生 明示的代入のみ
変数(上書き可能) リテラル(上書き不可)
typeof 結果 "undefined" "object"
JSON 化 プロパティ削除 nullとして保持

💡 具体例で理解する

// 1. 自然発生 vs 明示的代入
let a; // undefined(自然発生)
let b = null; // null(明示的代入)

// 2. オブジェクトプロパティ
const obj = {};
console.log(obj.name); // undefined(存在しないプロパティ)

// 3. 関数の戻り値
function greet() {}
console.log(greet()); // undefined(return文なし)

// 4. typeof演算子の違い
typeof undefined; // "undefined"
typeof null; // "object" ← 歴史的な仕様バグ

// 5. JSON化の違い
JSON.stringify({
  name: "John",
  age: undefined,
  avatar: null,
});
// 結果: '{"name":"John","avatar":null}'
// ageは削除される

⚠️ よくある誤解

// ❌ 間違った理解
if (value == null) {
  // これは undefined と null の両方をチェックする
}

// ✅ 正しい理解
if (value === null) {      // nullのみ
if (value === undefined) { // undefinedのみ
if (value == null) {       // 両方(推奨される場合もある)

2. 歴史・背景編:なぜ両方存在するのか

🕰️ JavaScript 誕生の背景(1995 年)

設計者の制約

  • Brendan Eich(JavaScript 作成者)が 10 日間で言語を設計
  • 上司から「JavaScript を Java のように見せろ」という命令
  • 当時は例外処理機能がなかった

設計上の判断

// Javaから借用した概念
null; // 「オブジェクトではない」値(Java由来)

// JavaScript独自の必要性
undefined; // 「原始値でもオブジェクトでもない」値

🎨 設計意図の詳細

1. Java との互換性

// Java風の書き方を可能にするため
String name = null;  // Java
var name = null;     // JavaScript(Java風)

2. 変数の状態表現

// 異なる「値がない」状態を区別
let user; // undefined: まだ初期化されていない
user = null; // null: 意図的に「値なし」を設定

🤔 現在の評価:設計ミス

JavaScript 作成者自身が認める問題

  • 2 つの「値なし」は混乱の元
  • 後方互換性により削除不可能
  • 現代では 1 つで十分だったと判明

🔄 なぜ削除されないのか

// 既存コードが大量に存在
if (document.getElementById("button") === null) {
  // 削除すると全世界のWebサイトが壊れる
}

JavaScript の鉄則:後方互換性を絶対に破らない


3. 実践編:どちらを使うべきか

🎯 結論:undefined を推奨

📈 Microsoft TypeScript 公式ガイドライン

「null は使わない」 - たった 1 行のシンプルなルール

🚀 undefined 推奨の理由

1. 自然な言語動作

// undefinedは自然に発生する
let user; // undefined
const obj = {};
console.log(obj.name); // undefined
function getName() {}
getName(); // undefined

// nullにするには余計な変換が必要
const converted = user ?? null; // 無駄な作業

2. チーム開発での一貫性

// ❌ 判断に迷うパターン
function getUser(): User | null | undefined {
  // どっちを返せばいい?混乱の元
}

// ✅ シンプルなパターン
function getUser(): User | undefined {
  // 迷わない
}

3. TypeScript との相性

// Optional Properties(推奨)
interface User {
  name: string;
  email?: string; // string | undefined
}

// 冗長なnull記述
interface UserBad {
  name: string;
  email: string | null | undefined; // 複雑
}

📝 実装パターン比較

// ✅ 推奨パターン
class UserService {
  async getUser(id: string): Promise<User | undefined> {
    try {
      const response = await fetch(`/api/users/${id}`);
      if (!response.ok) return undefined;
      return await response.json();
    } catch {
      return undefined;
    }
  }
}

// ❌ 非推奨パターン
class UserServiceBad {
  async getUser(id: string): Promise<User | null> {
    // nullを返す理由が不明確
    return null;
  }
}

4. 特殊ケース編:null が必要な場面

🎭 例外:React コンポーネントの条件付きレンダリング

React 特有の仕様

// ✅ 正しい:Reactコンポーネント非表示
function UserProfile({ user }: { user: User | undefined }) {
  if (!user) return null; // Reactの「何もレンダリングしない」仕様

  return <div>{user.name}</div>;
}

// ❌ 間違い:データ値でのnull使用
const [user, setUser] = useState<User | null>(null); // 避ける

データとレンダリングの区別

// データ値:undefined使用
const userData: User | undefined = getUser();

// React戻り値:null使用(仕様)
function Component() {
  return userData ? <UserProfile user={userData} /> : null;
}

🌐 外部システムとの連携

1. データベースの NULL 値

// Supabaseから返されるnull(仕様上受け入れ)
const { data } = await supabase.from("users").select("avatar_url").single();

// 内部的にundefinedに正規化
const avatarUrl = data.avatar_url ?? undefined;

2. 明示的な値削除

// APIで「このフィールドを削除して」を表現
const updateData = {
  name: "新しい名前",
  description: null, // 明示的削除
  // avatar: undefined だとJSONから除外される
};

// PATCH /api/users/123
// {"name":"新しい名前","description":null}

3. JSON 通信での意図表現

// フィールドの存在/削除を区別したい場合
interface UserUpdate {
  name?: string; // undefined = 更新しない
  avatar?: string | null; // null = 削除する, string = 更新
}

5. エコシステム別対応編:環境による使い分け

🏗️ 環境別推奨パターン

フロントエンド(TypeScript/React)

// ✅ undefined統一
interface UserState {
  data?: User;
  loading: boolean;
  error?: string;
}

const [user, setUser] = useState<User | undefined>();

Node.js(レガシーパターン)

// Error-first callback(歴史的理由でnull使用)
fs.readFile("/path", (err, data) => {
  if (err) return callback(err);
  callback(null, data); // null = エラーなし
});

// モダンなPromise/async-await
async function readFile(path) {
  try {
    const data = await fs.promises.readFile(path);
    return data;
  } catch {
    return undefined; // undefinedを推奨
  }
}

データベース(SQL)

-- データベースではNULL標準
SELECT name FROM users WHERE avatar_url IS NULL;
UPDATE users SET description = NULL; -- 明示的削除

GraphQL

# GraphQL仕様ではnull
type User {
  name: String!
  avatar: String # nullableフィールド
}

🔄 境界での変換戦略

// 外部システムとの境界で正規化
class APIService {
  // 外部APIから受信(nullを受け入れ)
  private async fetchExternal(): Promise<User | null> {
    // 外部API呼び出し
  }

  // 内部向けに正規化(undefinedに変換)
  async getUser(id: string): Promise<User | undefined> {
    const result = await this.fetchExternal();
    return result ?? undefined;
  }
}

6. 実装ガイド編:具体的なコーディング指針

📋 チェックリスト

✅ 推奨事項

  • 新しいコードではundefinedを使用
  • Optional Properties を活用(property?:
  • 外部 API からのnullは内部でundefinedに正規化
  • React コンポーネントでは条件的にnullを返す

❌ 避けるべき事項

  • 新規でnullを使った型定義
  • undefinednullを混在させる
  • 両方を許可する複雑な型(string | null | undefined

🔧 実装テンプレート

1. API 応答の型定義

// ✅ 推奨
interface ApiResponse<T> {
  data?: T;
  error?: string;
  loading: boolean;
}

// ❌ 非推奨
interface ApiBadResponse<T> {
  data: T | null | undefined; // 複雑
}

2. 状態管理(React + jotai)

// ✅ 推奨
const userAtom = atom<User | undefined>(undefined);

// 使用例
function useUser() {
  const [user, setUser] = useAtom(userAtom);

  const fetchUser = async (id: string) => {
    try {
      const response = await fetch(`/api/users/${id}`);
      const userData = response.ok ? await response.json() : undefined;
      setUser(userData);
    } catch {
      setUser(undefined);
    }
  };

  return { user, fetchUser };
}

3. Utility 関数

// null/undefined統一チェック
function isEmpty(value: unknown): value is null | undefined {
  return value == null; // 両方をチェック
}

// undefined正規化
function normalize<T>(value: T | null): T | undefined {
  return value ?? undefined;
}

// デフォルト値提供
function withDefault<T>(value: T | undefined, defaultValue: T): T {
  return value ?? defaultValue;
}

4. 条件付きレンダリングパターン

// ✅ 推奨:コンポーネント内で判断
function UserProfile({ user }: { user: User | undefined }) {
  if (!user) return null; // React仕様

  return (
    <div>
      <h1>{user.name}</h1>
      <img src={user.avatar ?? "/default-avatar.png"} />
    </div>
  );
}

// ✅ 親コンポーネントでの制御も可能
function App() {
  const user = useUser();

  return (
    <div>
      <Header />
      {user && <UserProfile user={user} />}
      <Footer />
    </div>
  );
}

🎯 段階的移行戦略

Phase 1: 新規コード

  • 新しく書くコードはundefined統一
  • 外部ライブラリのnullは受け入れつつ内部で正規化

Phase 2: 境界最適化

  • API 境界でnullundefined変換層を追加
  • 型定義を徐々にundefinedベースに更新

Phase 3: レガシー改善

  • 既存コードのリファクタリング(破壊的変更に注意)
  • チーム内でのコーディング規約統一

📚 さらなる学習リソース


🎉 まとめ

  1. 基本方針: undefinedを推奨、nullは特定用途のみ
  2. React: データはundefined、コンポーネント戻り値はnull
  3. 外部連携: 境界で正規化、内部はundefined統一
  4. チーム開発: シンプルなルールで一貫性を保つ

最重要ポイント: 完璧を目指さず、新しいコードから段階的に改善していく姿勢が大切です。

おわり 😊