null は「値が存在しない」ことを表します。ただし実際のコードでは、まったく違う事情がすべて null に押し込まれています。

  • まだ設定されていない
  • 検索したが見つからなかった
  • 意図的に空にした
  • 取得に失敗した
  • 参照する権限がない

これらを 1 つの値で表すと、受け取った側は「なぜ無いのか」を区別できません。現代の言語はこの曖昧さを削る方向に進んでいますが、その解き方は同じではありません。

4 つのモデル

値の欠如をどう表現するかで、言語は大きく 4 つに分かれます。

モデル 代表 書き方 欠如が検査される時点
null を通常の値として許す JavaScript null / undefined 実行時(規約頼み)
nullable を型で分離 TypeScript, PHP, Python User | null, ?User 静的検査時 / 実行時
直和型で表す Rust Option<User> コンパイル時(分解が必須)
API 上の入れ物で包む Java Optional<User> 実行時(null 自体は残る)

重要なのは記号の違いではありません。値を使う前に、無い場合の処理を書いたかどうかを誰が確かめるか——そこが違います。

nullable 型は「分離」であって「強制」ではない

TypeScript と PHP は、値がある型と無い型を別の型として書き分けます。

function findUser(id: number): User | null {
  return null;
}
function findUser(int $id): ?User
{
    return null;
}

見た目は似ていますが、効き方が違います。TypeScript の strictNullChecks では、User | null のまま user.name と書くとコンパイルエラーになります。検査は開発時に済み、実行時に型は残りません。

PHP の ?User は逆に実行時に検査されます。宣言と違う値を返せば TypeError になりますが、それはコードが動いた瞬間の話です。

Python の User | None は、注釈を書いただけでは何も起きません。CPython は型注釈で自動的にエラーを出さないからです。mypy や Pyright を通して初めて検査になります。

def find_user(user_id: int) -> User | None:
    return None

つまり同じ「nullable を型で分ける」でも、保証の強さは三段階あります。TypeScript は開発時に止まり、PHP は実行時に落ち、Python は型検査器を通したときだけ効きます。

Option 型は「分解しないと使えない」

Rust は値の有無を通常の型として表します。

enum Option<T> {
    Some(T),
    None,
}

fn find_user(id: u64) -> Option<User> {
    None
}

決定的なのは、Option<User> から User を取り出すには必ず分解する必要がある点です。

match find_user(1) {
    Some(user) => println!("{}", user.name),
    None => println!("not found"),
}

nullable 型が「null かもしれないと表示する」のに対し、Option 型は「null の場合の処理を書かせる」。表示と強制の差だと考えると分かりやすいです。

null 安全構文は書き味の問題

一方で、?.?? といった構文はどの言語でも似ています。

$country = $user?->address?->country ?? 'unknown';
const country = user?.address?.country ?? "unknown";
let name = optional_name.unwrap_or("guest");

これらは記号が違うだけで、意味は共通しています。値があれば処理し、無ければ安全に別の値へ分岐する。言語間で本当に違うのは、この分岐を書き忘れたときに誰が気づくかであって、演算子の見た目ではありません。

まとめ

  • null は「まだ無い」「見つからない」「失敗した」を区別せずに潰してしまう。曖昧さの原因はここにあります。
  • 「値がない」の表現は 4 つに分かれます。null をそのまま許す・型で分離する・直和型にする・入れ物で包む。
  • nullable 型の保証の強さは言語ごとに違います。TypeScript は開発時、PHP は実行時、Python は型検査器を使ったときだけ効きます。
  • Rust の Option<T> は分解を義務づけるため、欠損の処理漏れがコンパイル時に残りません。
  • ?.?? は書き味の話です。言語選択で効いてくるのは、書き忘れを検出する時点のほうです。