TypeScriptの開発において、nullとundefinedの扱いは、アプリケーションの堅牢性を左右する極めて重要な要素です。

JavaScriptから引き継がれたこの2つの「値の不在」を意味する型は、一見すると似ていますが、その設計思想や言語仕様上の振る舞いには明確な違いが存在します。

本記事では、2026年現在のモダンなTypeScript開発におけるnullとundefinedの最適な使い分けと、型安全を維持するための実装パターンについて詳しく掘り下げていきます。

nullとundefinedの基本的な定義と違い

TypeScriptにおけるnullとundefinedは、どちらも「値が存在しない状態」を表しますが、そのニュアンスは大きく異なります。

まず、undefinedは「値がまだ割り当てられていない」という、システム側が自動的に付与するデフォルトの状態を指します。

一方で、nullは「値が意図的に欠落している」という、開発者が明示的に設定する状態を指すのが一般的です。

この違いを理解することは、コードの意図を他の開発者に正確に伝えるための第一歩となります。

JavaScriptの仕様としての違い

JavaScriptの言語仕様において、これら2つは異なるプリミティブ型として定義されています。

以下のコードを使用して、それぞれの型をtypeof演算子で確認してみましょう。

TypeScript
// undefinedの確認
const valUndefined = undefined;
console.log(typeof valUndefined);

// nullの確認
const valNull = null;
console.log(typeof valNull);
実行結果
"undefined"
"object"

ここで注目すべき点は、typeof nullの結果が “object” になるというJavaScriptの歴史的な仕様です。

これはJavaScriptの初期実装におけるバグの名残ですが、現在も互換性のために維持されています。

この性質があるため、単純なtypeofチェックだけでnullを判定しようとすると、他のオブジェクトと混同する可能性があるため注意が必要です。

TypeScriptにおける型としての扱い

TypeScriptでは、これらをリテラル型として厳密に区別して扱うことができます。

特に重要なのが、tsconfig.jsonにおけるstrictNullChecksオプションの設定です。

このオプションが有効な場合、string型の変数にnullundefinedを代入することはできなくなります。

型安全性を最大限に高めるためには、このオプションを常に有効にすることが2026年現在の標準的な開発スタイルとなっています。

実務における使い分けのガイドライン

プロジェクト全体でnullとundefinedの使い分けに一貫性を持たせることは、メンテナンス性の向上に直結します。

一般的に推奨される指針として、「基本的にはundefinedを使用し、外部ライブラリやAPIの仕様上必要な場合のみnullを使用する」という考え方があります。

undefinedを優先する理由

TypeScriptの多くの言語機能は、undefinedと親和性が高く設計されています。

例えば、関数の省略可能な引数 (Optional Parameters) や、インターフェースの省略可能なプロパティ (Optional Properties) は、自動的にundefinedを許容するようになります。

TypeScript
interface User {
  id: number;
  nickname?: string; // nicknameは string | undefined と同じ
}

const userA: User = { id: 1 };
console.log(userA.nickname);
実行結果
undefined

このように、値が存在しない可能性を?記法で表現すると自動的にundefinedが割り当てられるため、あえてnullを混ぜる必要性が低くなります。

また、JavaScriptの仕様でも、値を返さない関数の戻り値はデフォルトでundefinedとなります。

nullを使用すべきケース

一方で、nullを積極的に使用すべき場面も存在します。

それは、データベースのフィールドが「値なし (NULL) 」を許容している場合や、外部のWeb APIがJSONレスポンスとしてnullを返してくる場合です。

このような外部データとのやり取りにおいては、データの忠実性を保つためにnullをそのまま扱うのが適切です。

また、「意図的にクリアされた値」であることを強調したい場合に、開発者の意図としてnullを代入するパターンも見られます。

JSONシリアライズにおける挙動の違い

nullとundefinedの決定的な違いの一つに、JSONに変換した際の挙動があります。

この違いは、フロントエンドとバックエンドの間でデータを送受信する際に大きな影響を与えます。

TypeScript
const data = {
  a: null,
  b: undefined,
  c: "hello"
};

console.log(JSON.stringify(data));
実行結果
{"a":null,"c":"hello"}

実行結果からわかるように、undefinedを持つプロパティはJSONから完全に除外されます。

一方で、nullを持つプロパティはそのままnullとして残ります。

APIのペイロードにおいて、「プロパティ自体は存在するが値が空である」ことを明示したい場合は、nullを使用する必要があります。

型安全にnull/undefinedを処理するテクニック

TypeScriptには、nullやundefinedを安全かつ簡潔に扱うための便利な演算子が多数用意されています。

これらの演算子をマスターすることで、煩雑なif文によるチェックを減らし、可読性の高いコードを書くことができます。

オプショナルチェイニング (Optional Chaining)

?.演算子を使用すると、プロパティがnullまたはundefinedであってもランタイムエラーを防ぎながら安全にアクセスできます。

TypeScript
type Profile = {
  address?: {
    city: string;
  };
};

const profile: Profile = {};
// addressがなくてもエラーにならず、undefinedを返す
const city = profile.address?.city;
console.log(city);
実行結果
undefined

深い階層のオブジェクトを扱う際に、この演算子は必須のツールとなります。

Null合体演算子 (Nullish Coalescing)

??演算子は、左辺の値がnullまたはundefinedのときだけ右辺の値を返します。

従来の||(論理和演算子) との違いを理解しておくことが重要です。

TypeScript
const count = 0;

const result1 = count || 10; // 0は偽値なので10になる
const result2 = count ?? 10; // 0はnull/undefinedではないので0になる

console.log(`OR: ${result1}, Nullish: ${result2}`);
実行結果
OR: 10, Nullish: 0

数値の0や空文字 "" を有効な値として扱いたい場合は、必ずNullish Coalescing (??) を使用するようにしましょう。

型ガードによる絞り込み

TypeScriptのコンパイラに対して、値がnullやundefinedでないことを保証するために「型ガード」を使用します。

TypeScript
function processValue(val: string | null | undefined) {
  if (val != null) {
    // このブロック内では val は string 型に絞り込まれる
    console.log(val.toUpperCase());
  } else {
    console.log("値が存在しません");
  }
}

ここで val != null という比較を使うと、nullとundefinedの両方を一度にチェックできるため便利です。

厳密等価演算子 !== を使わずに != を使う数少ない正当なケースの一つと言えます。

高度な実装パターン:Discriminated Unionsの活用

複雑な状態管理においては、単に string | null と定義するよりも、状態を明示的に分ける「判別可能な共用体 (Discriminated Unions) 」の利用が推奨されます。

TypeScript
type FetchState<T> = 
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function render(state: FetchState<string>) {
  switch (state.status) {
    case "loading":
      return "読み込み中...";
    case "success":
      // ここでは安全に data にアクセスできる
      return state.data;
    case "error":
      return state.error.message;
  }
}

このように設計することで、「値がまだ無い状態」と「取得に失敗して値が無い状態」を明確に区別でき、ロジックの漏れをコンパイル時点で防ぐことが可能になります。

非nullアサーション演算子のリスク

!演算子 (Non-null Assertion Operator) は、開発者がコンパイラに対して「この値は絶対にnullでもundefinedでもない」と宣言するものです。

しかし、これは型安全性を強制的にバイパスする手段であり、実行時のランタイムエラーを引き起こすリスクを高めます。

2026年のモダンな開発においては、可能な限りこの演算子の使用を避け、前述の型ガードやオプショナルチェイニングで解決すべきです。

テストコードや特定のDOM操作など、どうしても避けられない状況を除き、!の使用はコードの不吉な臭い (Code Smell) と見なされることが多いです。

まとめ

TypeScriptにおけるnullとundefinedの違いを正しく理解し、適切に使い分けることは、高品質なコードを書くための基盤となります。

undefinedは「未定義」というシステム的な状態に、nullは「空」という意図的な状態にそれぞれ割り当てるのが基本的なプラクティスです。

特にstrictNullChecksを有効にした環境では、これらの型を厳密に扱うことが求められます。

オプショナルチェイニングやNull合体演算子を活用し、型ガードによって安全に型を絞り込むことで、予期せぬエラーの少ないプログラムを構築できます。

プロジェクトのコーディング規約として、これらの使い分けを明文化しておくことも、チーム開発を円滑に進める上で非常に効果的です。

最新のTypeScript機能を駆使して、安全で読みやすいコードを追求していきましょう。