TypeScriptにおけるプログラミングにおいて、値が存在しない状態をいかに安全に扱うかは、アプリケーションの堅牢性を左右する重要なテーマです。

特にモダンなフロントエンド開発やサーバーサイド開発では、外部データやオプション設定など、実行時まで値が確定しない変数を扱う場面が数多く存在します。

このような不確実性をスマートに解決するために導入されたのが、Null合体演算子(??)という機能です。

本記事では、Null合体演算子の基本的な使い方から、従来の論理和演算子(||)との決定的な違い、そして安全な初期化コードを書くための実践的なテクニックを詳しく紹介します。

Null合体演算子(??)の基本概念と構文

Null合体演算子(??)は、左辺の値が null または undefined の場合にのみ、右辺の値を返す論理演算子です。

逆に言えば、左辺が null でも undefined でもない場合は、そのまま左辺の値を評価結果として採用します。

この演算子は、TypeScript 3.7から導入された機能であり、現代のJavaScript開発(ES2020以降)においても標準的な機能となっています。

基本的な構文は、左辺 ?? 右辺 という非常にシンプルな形式で記述されます。

具体的なコード例を確認してみましょう。

TypeScript
// 変数が undefined の場合
const userName: string | undefined = undefined;
const displayName = userName ?? "ゲストユーザー";

console.log(displayName);

// 変数が null の場合
const userEmail: string | null = null;
const contactEmail = userEmail ?? "info@example.com";

console.log(contactEmail);
実行結果
ゲストユーザー
info@example.com

このように、変数が「空の状態」であるときに、安全なデフォルト値を代入する目的で使用されます。

TypeScriptの型システムにおいても、この演算子を通すことで null | undefined の可能性を排除できるため、型安全性を高める効果があります。

OR演算子(||)との決定的な違い

TypeScriptには古くから、デフォルト値を設定する手法として論理和演算子(||)が利用されてきました。

しかし、論理和演算子(||)とNull合体演算子(??)には、動作上の重大な違いが存在します。

論理和演算子は、左辺が 「Falsy(偽値)」 である場合に右辺を返します。

JavaScript/TypeScriptにおけるFalsyな値には、nullundefined だけでなく、数値の 0、空文字列 ""、そして false が含まれます。

一方で、Null合体演算子は 「Nullish(nullまたはundefined)」 な場合のみ右辺を評価します。

数値「0」を扱う場合の挙動の違い

この違いが最も顕著に現れるのが、数値を扱うシーンです。

設定値として 0 を許容したい場合、論理和演算子(||)を使用すると予期せぬ挙動を引き起こす可能性があります。

TypeScript
const retryCount = 0;

// OR演算子(||)の場合
const resultWithOr = retryCount || 5; 
console.log(`OR演算子の結果: ${resultWithOr}`);

// Null合体演算子(??)の場合
const resultWithNullish = retryCount ?? 5;
console.log(`Null合体演算子の結果: ${resultWithNullish}`);
実行結果
OR演算子の結果: 5
Null合体演算子の結果: 0

上記の例では、論理和演算子を用いると 0 がFalsyと判定され、デフォルト値の 5 が採用されてしまいます。

もしプログラムの仕様として「リトライ回数 0回」を有効な設定としたい場合、これはバグの原因となります。

Null合体演算子であれば、0null でも undefined でもないため、正しく 0 が保持されます。

空文字列「””」を扱う場合の挙動の違い

文字列の初期化においても同様の注意が必要です。

ユーザーが入力した空文字列を有効な値として扱いたい場合、論理和演算子は不適切です。

TypeScript
const userInput = "";

const nameWithOr = userInput || "名無しさん";
const nameWithNullish = userInput ?? "名無しさん";

console.log(`OR: ${nameWithOr}`);
console.log(`Nullish: ${nameWithNullish}`);
実行結果
OR: 名無しさん
Nullish:

論理和演算子では空文字列が無視され、デフォルト値に上書きされてしまいます。

「値が存在しないこと」と「空の値を明示すること」を区別したい場合、必ずNull合体演算子を選択する必要があります。

実践的な活用シーン:安全な初期化コード

実際のアプリケーション開発において、Null合体演算子がどのように活用されるかを具体的に見ていきましょう。

1. オプション設定(Configuration)のパース

関数の引数としてオプションオブジェクトを受け取る際、一部のプロパティが省略されることはよくあります。

そのような場合に、不足している項目だけをデフォルト値で補完する実装が非常に簡潔に書けます。

TypeScript
interface AppConfig {
  timeout?: number;
  showLogs?: boolean;
  theme?: "light" | "dark";
}

function initializeApp(config: AppConfig) {
  // 設定が省略された場合のみデフォルト値を適用
  const timeout = config.timeout ?? 3000;
  const showLogs = config.showLogs ?? true;
  const theme = config.theme ?? "light";

  console.log(`設定適用: Timeout=${timeout}, Logs=${showLogs}, Theme=${theme}`);
}

// 部分的に指定して呼び出し
initializeApp({ timeout: 0, showLogs: false });
実行結果
設定適用: Timeout=0, Logs=false, Theme=light

この実装では、ユーザーが明示的に timeout: 0showLogs: false を指定した場合でも、その意図が正しく反映されます。

2. APIレスポンスのハンドリング

APIから取得したデータは、特定のフィールドが欠損していたり、null で返ってきたりすることがあります。

UIに表示する直前で、ユーザーに不親切な null 表示を避けるために活用します。

TypeScript
interface UserProfile {
  bio: string | null;
  followersCount?: number;
}

const apiResponse: UserProfile = {
  bio: null,
  followersCount: 0
};

const displayBio = apiResponse.bio ?? "自己紹介はまだありません。";
const displayFollowers = apiResponse.followersCount ?? "データなし";

console.log(displayBio);
console.log(displayFollowers);
実行結果
自己紹介はまだありません。
0

オプショナルチェイニング(?.)との組み合わせ

Null合体演算子は、オプショナルチェイニング演算子(?.)と組み合わせることで、その真価を発揮します。

オプショナルチェイニングは、深いネスト構造にあるプロパティに安全にアクセスし、途中の経路が null または undefined であれば即座に undefined を返す仕組みです。

これらを組み合わせることで、「深い階層のデータアクセスからデフォルト値の適用まで」 を1行で安全に記述できるようになります。

TypeScript
interface Settings {
  user?: {
    preferences?: {
      fontSize?: number;
    }
  }
}

const settings: Settings = {};

// 従来の書き方(冗長でミスが発生しやすい)
const sizeLegacy = settings.user && settings.user.preferences && settings.user.preferences.fontSize !== undefined 
  ? settings.user.preferences.fontSize 
  : 16;

// モダンな書き方(簡潔で安全)
const sizeModern = settings.user?.preferences?.fontSize ?? 16;

console.log(sizeModern);
実行結果
16

この記法は可読性が高く、意図が明確であるため、現代のTypeScript開発におけるベストプラクティスの一つとされています。

TypeScriptの型推論とNull合体演算子

TypeScriptのコンパイラは、Null合体演算子の挙動を正確に理解し、型推論を行います。

左辺の型が string | null | undefined であり、右辺が string 型である場合、演算結果は自動的に string 型として推論されます。

TypeScript
function getLength(text: string | null): number {
  // ?? を使うことで、textがnullでないことが保証される
  const safeText = text ?? "";
  
  // ここで safeText は string 型として確定しているため、lengthプロパティに安全にアクセス可能
  return safeText.length;
}

このように、複雑な if 文による型ガード(Type Guard)を書かなくても、型安全なコードを維持できるのが大きなメリットです。

注意点:優先順位と混合使用の制限

Null合体演算子を使用する際には、構文上のルールに注意が必要です。

特に、論理積演算子(&&)や論理和演算子(||)と直接組み合わせて使用する場合、カッコ () による優先順位の明示が必須となります。

これは、演算子の優先順位による予期せぬ誤解を防ぐための言語仕様上の制約です。

TypeScript
// コンパイルエラーになる例
// const result = a || b ?? c; 

// 正しい記述方法
const a = null;
const b = false;
const c = "default";

const result = (a || b) ?? c;
console.log(result);
実行結果
default

エラーを避けるためだけでなく、コードを読む他の開発者が演算の意図を正しく理解できるように、複雑な論理式では常にカッコを活用することを推奨します。

まとめ

TypeScriptのNull合体演算子(??)は、nullundefined を安全かつ簡潔に扱うための非常に強力なツールです。

従来の論理和演算子(||)が持っていた「0や空文字列、falseを意図せず上書きしてしまう」という副作用を回避し、開発者の意図に忠実なデフォルト値の設定が可能になります。

「厳密に値が存在しない場合のみ、代替値を提供したい」 というシーンでは、常にこの演算子を優先して使用しましょう。

また、オプショナルチェイニング(?.)と併用することで、ネストされた複雑なオブジェクトの操作も劇的にシンプルになります。

現代のTypeScript開発において、これらの演算子をマスターすることは、バグの少ない高品質なコードを書くための第一歩です。

日々のコーディングの中で、Falsyな値とNullishな値の違いを意識し、適切な演算子を選択する習慣を身につけていきましょう。