TypeScriptのnull・undefined判定の正解|書き分けパターンとモダンな回避策

Typescript
記事内に広告が含まれています。

TypeScriptで開発していると、「Object is possibly 'undefined'」「nullundefined の違いが分からない」「結局どう判定するのが正解なのか…」と悩んだ経験はありませんか。

JavaScriptの感覚で if (value)value === undefined と書いていたら、TypeScriptでは思わぬコンパイルエラーが出たり、レビューで指摘されたりすることも少なくありません。特に nullundefined・空文字・0 が絡む条件分岐は、少し書き方を間違えるだけでバグの温床になりがちです。

さらに、Optional Chaining(?.)や Nullish Coalescing(??)といった新しい構文が登場したことで、「便利そうだけど正しく使えている自信がない」「|| との違いがあいまい」という方も多いのではないでしょうか。TypeScriptは型安全を高めるために nullundefined を厳密に区別しますが、その理由や設計思想を理解しないまま使うと、エラーを“消すだけのコード”になってしまいます。

この記事では、「typescript null undefined 判定」で検索した方がつまずきやすいポイントを整理しながら、実務でそのまま使える安全な判定方法を分かりやすく解説します。場当たり的な対処ではなく、「なぜその書き方が正しいのか」まで理解できる内容を目指しています。

この記事を読んでわかること

  • TypeScriptにおける nullundefined の明確な違いと設計思想
  • value === null / value === undefined / value == null の正しい使い分け
  • Object is possibly 'undefined' エラーが出る本当の理由と根本的な解決策
  • if (value)!!value が危険と言われる理由と安全な代替案
  • 空文字・0NaN を含めた実務向けの条件分岐パターン
  • Optional Chaining(?.)や Nullish Coalescing(??)の正しい使いどころ
  • TypeScriptでレビューに指摘されない、読みやすく安全な判定の書き方
格安ドメイン取得サービス─ムームードメイン─
  1. TypeScriptでnullとundefinedを判定する前に知るべき基礎知識
    1. JavaScriptとTypeScriptで異なる「値がない」ことの定義
    2. なぜTypeScriptはnullとundefinedを厳密に区別するのか
    3. strictNullChecksとは何か|ON/OFFで何が変わるのか
    4. strictNullChecks: false の場合(レガシーモード)
    5. strictNullChecks: true の場合(推奨設定)
  2. TypeScriptにおけるnull・undefinedの代表的な判定パターン
    1. value === null と value === undefined の正しい使い分け
    2. 「===」ではなく「==」を唯一使っても良いケースとその理由
    3. typeof value === “undefined” が必要になるケース
  3. 空文字・0・NaNを含めるか?条件別の「falsy判定」使い分け一覧
    1. null / undefined / 空文字を一括判定したいときの条件分岐パターン集
    2. if (value) が危険な理由|0やfalseが弾かれる落とし穴
    3. Boolean(value) や !!value を使うときにハマりやすい落とし穴と回避策
  4. TypeScriptの新常識!Optional Chaining(?.)とNullish Coalescing(??)
    1. Optional Chaining(?.)でundefinedアクセスを安全に回避
    2. Nullish Coalescing(??)と論理OR(||)の決定的な違い
    3. 最新TypeScriptで推奨される簡潔で可読性の高い判定構文
  5. よくある質問
  6. まとめ
    1. 基本原則:判定方法の選択フローチャート
    2. 実務で頻出するパターン別ベストプラクティス
    3. 避けるべきアンチパターン
    4. 判定方法早見表
    5. TypeScriptのバージョン別機能対応表
    6. チーム開発で統一すべきコーディング規約

TypeScriptでnullとundefinedを判定する前に知るべき基礎知識

JavaScriptとTypeScriptで異なる「値がない」ことの定義

JavaScriptには「値がない」ことを表現する方法が複数存在します。主なものはnullundefined、そして空文字""です。これらは一見似ているようで、言語仕様上は明確に異なる存在として扱われます。

let a: string | null = null;           // 明示的に「値が存在しない」
let b: string | undefined = undefined; // 未定義、または初期化されていない
let c: string = "";                    // 空の文字列(値は存在する)

console.log(typeof a); // "object"
console.log(typeof b); // "undefined"
console.log(typeof c); // "string"

JavaScriptでは、nullは「意図的に値がないことを示すオブジェクト」として扱われ、typeof nullは歴史的な理由から"object"を返します。一方、undefinedは「変数が宣言されたが値が代入されていない」状態を表します。

TypeScriptはこの区別をさらに厳密化し、型システムに組み込んでいます。実務では以下のような使い分けが一般的です。

interface User {
  name: string;
  email: string;
  phoneNumber: string | null;  // 電話番号は任意。ない場合はnull
  avatar?: string;              // アバター画像は省略可能(undefined許容)
}

const user: User = {
  name: "山田太郎",
  email: "yamada@example.com",
  phoneNumber: null,  // 電話番号を登録していない
  // avatarは省略(undefinedになる)
};

このコードでは、phoneNumberは明示的に「登録されていない」ことをnullで表現し、avatarはプロパティ自体が省略可能であることを?(Optional Property)で表現しています。

なぜTypeScriptはnullとundefinedを厳密に区別するのか

TypeScriptがnullundefinedを厳密に区別する理由は、型安全性を高めるためです。JavaScriptでは両者を曖昧に扱えますが、TypeScriptでは「意図的な値の不在(null)」と「未初期化・未定義(undefined)」を型レベルで識別できます。

// Before: JavaScriptの曖昧な扱い
function getUserName(user) {
  return user.name;  // userがnullやundefinedの場合、実行時エラー
}

// After: TypeScriptの型安全な扱い
function getUserName(user: User | null): string {
  if (user === null) {
    return "ゲスト";
  }
  return user.name;  // この時点でuserはUser型として確定
}

この区別により、以下のメリットが得られます。

  1. コンパイル時のエラー検出: nullundefinedの可能性がある変数に対して、チェックなしでプロパティアクセスするとコンパイルエラーになります
  2. 意図の明確化: APIのレスポンスでnullが返ってきた場合は「データが存在しない」、undefinedの場合は「そのフィールド自体が含まれていない」といった意味の違いを表現できます
  3. 型の絞り込み(Type Narrowing): 条件分岐でnullチェックを行うと、その後のスコープでは型が自動的に絞り込まれます
interface APIResponse {
  data: User | null;      // データがない場合はnull
  error?: string;         // エラーメッセージは任意
}

function handleResponse(response: APIResponse) {
  if (response.data === null) {
    console.log("データが見つかりません");
    return;
  }

  // この時点でresponse.dataはUser型として扱える
  console.log(response.data.name);  // 型補完が効く

  if (response.error !== undefined) {
    // errorが存在する場合の処理
    console.error(response.error);
  }
}

strictNullChecksとは何か|ON/OFFで何が変わるのか

TypeScriptのstrictNullChecksは、nullundefinedの扱い方を決定する最も重要なコンパイラオプションです。tsconfig.jsonで設定できます。

{
  "compilerOptions": {
    "strictNullChecks": true  // または "strict": true に含まれる
  }
}

strictNullChecks: false の場合(レガシーモード)

// strictNullChecks: false
let name: string = null;        // エラーにならない
let age: number = undefined;    // エラーにならない

function greet(user: User) {
  console.log(user.name);  // userがnullでもコンパイルエラーにならない
}

greet(null);  // 実行時エラーになる可能性があるが、コンパイルは通る

strictNullChecksが無効の場合、すべての型に暗黙的にnullundefinedが含まれます。これはJavaScriptとの互換性は高いものの、型安全性が大幅に低下します。

strictNullChecks: true の場合(推奨設定)

// strictNullChecks: true
let name: string = null;        // ❌ Error: Type 'null' is not assignable to type 'string'
let age: number = undefined;    // ❌ Error: Type 'undefined' is not assignable to type 'number'

// 明示的にnullを許容する必要がある
let name2: string | null = null;           // ✅ OK
let age2: number | undefined = undefined;  // ✅ OK

function greet(user: User | null) {
  console.log(user.name);  // ❌ Error: Object is possibly 'null'

  if (user !== null) {
    console.log(user.name);  // ✅ OK(型ガードで絞り込まれている)
  }
}

実務における推奨設定

モダンなTypeScriptプロジェクトでは、strictNullChecks: true(またはstrict: true)が標準です。以下の表で挙動の違いをまとめます。

項目strictNullChecks: falsestrictNullChecks: true
string型にnull代入⭕ 可能❌ エラー
User型にnull代入⭕ 可能❌ エラー
null/undefinedチェック不要でプロパティアクセス⭕ 可能(危険)❌ エラー
型の明示性低い高い
実行時エラーの可能性高い低い
推奨度非推奨強く推奨
// strictNullChecks: true での実践的な例
interface Product {
  id: number;
  name: string;
  description: string | null;  // 説明文は任意
  stock?: number;               // 在庫数は省略可能
}

function displayProduct(product: Product | null) {
  // productのnullチェック
  if (product === null) {
    console.log("商品が見つかりません");
    return;
  }

  // descriptionのnullチェック
  const desc = product.description ?? "説明なし";

  // stockのundefinedチェック
  const stockInfo = product.stock !== undefined
    ? `在庫: ${product.stock}個`
    : "在庫情報なし";

  console.log(`${product.name}: ${desc} (${stockInfo})`);
}

strictNullChecksを有効にすることで、コンパイル時に潜在的なバグを発見でき、コードレビューでも「なぜnullチェックがないのか」といった指摘を受けにくくなります。新規プロジェクトでは必ず有効化し、既存プロジェクトでも段階的に移行することを強く推奨します。

TSConfig Reference - Docs on every TSConfig option
From allowJs to useDefineForClassFields the TSConfig reference includes information about all of the active compiler flags setting up a TypeScript project.

コストパフォーマンスに優れた高性能なレンタルサーバー

【Hostinger】

TypeScriptにおけるnull・undefinedの代表的な判定パターン

value === null と value === undefined の正しい使い分け

TypeScriptでnullとundefinedを判定する最も基本的かつ推奨される方法は、厳密等価演算子===を使った比較です。これにより、型の曖昧さを排除し、意図を明確にできます。

function processUser(user: User | null | undefined) {
  // nullの判定
  if (user === null) {
    console.log("ユーザーは明示的に存在しません");
    return;
  }

  // undefinedの判定
  if (user === undefined) {
    console.log("ユーザーは未設定です");
    return;
  }

  // この時点でuserはUser型として確定
  console.log(user.name);  // 型補完が効く
}

使い分けの実践的な指針

interface APIResponse {
  user: User | null;  // データがない場合はnull
  settings?: {        // settingsプロパティ自体が省略可能
    theme: string;
  };
}

function handleAPIResponse(response: APIResponse) {
  // nullチェック:APIが「ユーザーが存在しない」と明示的に返した
  if (response.user === null) {
    console.log("ユーザーが見つかりませんでした");
    return;
  }

  // undefinedチェック:オプショナルなプロパティの存在確認
  if (response.settings === undefined) {
    console.log("設定が未定義です");
    // デフォルト設定を使用
  } else {
    console.log(`テーマ: ${response.settings.theme}`);
  }

  console.log(`ユーザー名: ${response.user.name}`);
}

型ガードとしての効果

===による判定は、TypeScriptの型システムに正しく認識され、コントロールフロー分析(Control Flow Analysis)によって型が自動的に絞り込まれます。

function getDisplayName(name: string | null | undefined): string {
  if (name === null) {
    return "名前なし";
  }

  if (name === undefined) {
    return "未設定";
  }

  // この時点でnameはstring型として確定
  return name.toUpperCase();  // nullやundefinedの可能性がないため安全
}

「===」ではなく「==」を唯一使っても良いケースとその理由

JavaScriptの==(等価演算子)は型強制を伴うため、一般的には避けるべきとされています。しかし、nullとundefinedの両方を同時に判定する場合に限り== nullの使用が公式に推奨されています。

// ❌ 冗長な書き方
if (value === null || value === undefined) {
  console.log("値が存在しません");
}

// ✅ 推奨される簡潔な書き方
if (value == null) {
  console.log("値が存在しません");
}

なぜ== nullは例外的に許容されるのか

JavaScriptの仕様では、==を使った場合、以下の挙動が保証されています。

null == undefined  // true
null == null       // true
undefined == undefined  // true

// それ以外の値はすべてfalse
0 == null          // false
"" == null         // false
false == null      // false

つまり、value == nullは「valueがnullまたはundefinedの場合のみtrue」となり、他のfalsy値(0、””、falseなど)を誤って判定することがありません。

実務での活用例

interface FormData {
  username: string;
  email: string;
  phoneNumber?: string;  // 省略可能
  address: string | null;  // nullの可能性がある
}

function validateFormData(data: FormData) {
  // phoneNumberとaddressの両方をチェック
  // どちらかがnullまたはundefinedの場合に警告
  if (data.phoneNumber == null || data.address == null) {
    console.warn("一部の情報が未入力です");
  }

  // より詳細な判定が必要な場合は === を使う
  if (data.address === null) {
    console.log("住所が明示的にnullです");
  } else if (data.address === undefined) {
    console.log("住所が未定義です");  // 通常はここには来ない(型定義上nullのみ)
  }
}

ESLintでの扱い

多くのプロジェクトで使われるESLintのeqeqeqルールでは、== nullのみを例外として許可するオプションがあります。

// .eslintrc.json
{
  "rules": {
    "eqeqeq": ["error", "always", { "null": "ignore" }]
  }
}

この設定により、== nullのみが許可され、その他の==の使用はエラーとなります。

判定方法の比較表

判定方法nullundefined0“”falseNaN推奨度
value === null⭐⭐⭐
value === undefined⭐⭐⭐
value == null⭐⭐⭐
!value⚠️ 注意
// 実践例:APIレスポンスの処理
async function fetchUserProfile(userId: string): Promise<User | null> {
  const response = await api.getUser(userId);

  // responseがnullまたはundefinedの場合を一括判定
  if (response == null) {
    console.log("ユーザーデータの取得に失敗しました");
    return null;
  }

  return response;
}

function displayUserInfo(user: User | null | undefined) {
  // nullとundefinedを区別する必要がない場合
  if (user == null) {
    console.log("ユーザー情報がありません");
    return;
  }

  // この時点でuserはUser型
  console.log(`名前: ${user.name}`);
  console.log(`メール: ${user.email}`);
}

typeof value === “undefined” が必要になるケース

通常、value === undefinedで十分ですが、変数が宣言されているかどうかが不明な場合にはtypeofを使う必要があります。

// ケース1:グローバル変数の存在確認
// ❌ これはReferenceErrorになる可能性がある
if (window.myGlobalVar === undefined) {
  // myGlobalVarが宣言されていない場合、ここでエラー
}

// ✅ 安全な確認方法
if (typeof window.myGlobalVar === "undefined") {
  console.log("myGlobalVarは存在しません");
}

実務での具体例

// ブラウザ環境かどうかの判定
if (typeof window !== "undefined") {
  // ブラウザ環境での処理
  window.localStorage.setItem("key", "value");
} else {
  // Node.js環境での処理
  console.log("サーバーサイドで実行中");
}

// Next.jsでのSSR対応コード
function useLocalStorage(key: string) {
  if (typeof window === "undefined") {
    // サーバーサイドレンダリング時
    return null;
  }

  // クライアントサイドのみで実行
  return window.localStorage.getItem(key);
}

ケース2:オプショナルプロパティの判定

通常の変数やプロパティに対しては、=== undefinedで十分です。

interface Config {
  apiUrl: string;
  timeout?: number;  // オプショナル
}

function createClient(config: Config) {
  // ✅ これで十分(通常の推奨方法)
  if (config.timeout === undefined) {
    config.timeout = 5000;  // デフォルト値を設定
  }

  // typeof は不要(プロパティは必ず存在するか undefined)
  // if (typeof config.timeout === "undefined") { } // 冗長
}

ケース3:外部ライブラリやAPIの型定義が不正確な場合

// 外部ライブラリの型定義が不完全な場合
declare const externalLib: any;

function useExternalFeature() {
  // ライブラリの特定の機能が存在するかチェック
  if (typeof externalLib.someFeature === "undefined") {
    console.log("この機能はサポートされていません");
    return;
  }

  externalLib.someFeature();
}

判定方法の選択フローチャート

// 判定方法の選択ガイド
function checkValue(value: unknown) {
  // 1. 変数が宣言されているか不明な場合(グローバル変数など)
  if (typeof someGlobalVar === "undefined") { }

  // 2. nullとundefinedを区別したい場合
  if (value === null) { }
  if (value === undefined) { }

  // 3. nullとundefinedをまとめて判定したい場合
  if (value == null) { }

  // 4. プロパティの存在確認(通常のケース)
  const obj: { prop?: string } = {};
  if (obj.prop === undefined) { }  // これで十分
}

まとめ:判定パターンの使い分け

// 実践的な統合例
interface Product {
  id: number;
  name: string;
  price: number | null;      // 価格未定の場合null
  description?: string;      // 説明文は省略可能
  stock?: number | null;     // 在庫情報は省略可能、または未確定
}

function displayProduct(product: Product | null | undefined) {
  // パターン1: nullとundefinedをまとめて判定
  if (product == null) {
    console.log("商品情報がありません");
    return;
  }

  // パターン2: nullのみを厳密に判定
  if (product.price === null) {
    console.log(`${product.name}: 価格は後日公開`);
  } else {
    console.log(`${product.name}: ¥${product.price.toLocaleString()}`);
  }

  // パターン3: undefinedのみを判定(オプショナルプロパティ)
  if (product.description === undefined) {
    console.log("説明文なし");
  } else {
    console.log(product.description);
  }

  // パターン4: nullとundefinedの両方があり得る場合
  if (product.stock == null) {
    console.log("在庫情報なし");
  } else if (product.stock === 0) {
    console.log("在庫切れ");
  } else {
    console.log(`在庫: ${product.stock}個`);
  }
}

これらのパターンを適切に使い分けることで、型安全性を保ちながら、可読性の高いコードを書けるようになります。

空文字・0・NaNを含めるか?条件別の「falsy判定」使い分け一覧

null / undefined / 空文字を一括判定したいときの条件分岐パターン集

実務では、nullとundefinedだけでなく、空文字列も「値がない」として扱いたい場面が頻繁にあります。フォームのバリデーションやユーザー入力の検証がその典型例です。

パターン1: 文字列の空チェック(最も一般的)

function validateUsername(username: string | null | undefined): boolean {
  // ❌ 誤った判定:空文字を見逃す
  if (username === null || username === undefined) {
    return false;
  }
  // usernameが""の場合はtrueを返してしまう

  // ✅ 正しい判定:null / undefined / 空文字をすべて弾く
  if (username == null || username === "") {
    return false;
  }

  return username.length >= 3;
}

// より簡潔な書き方
function validateUsername2(username: string | null | undefined): boolean {
  // 空文字の場合も!usernameでfalseになる
  if (!username) {
    return false;
  }

  // この時点でusernameはstring型(型ガードが効く)
  return username.length >= 3;
}

パターン2: trimを併用した実践的なバリデーション

interface FormInput {
  email: string;
  name: string;
  comment?: string;
}

function validateFormInput(input: FormInput): string[] {
  const errors: string[] = [];

  // emailの検証:null/undefined/空文字/空白文字のみを弾く
  if (!input.email || input.email.trim() === "") {
    errors.push("メールアドレスは必須です");
  }

  // nameの検証:同様に空白文字のみも弾く
  if (!input.name?.trim()) {
    errors.push("名前は必須です");
  }

  // commentの検証:オプショナルなので、値がある場合のみチェック
  if (input.comment !== undefined && input.comment.trim() === "") {
    errors.push("コメントは空白のみにできません");
  }

  return errors;
}

// 使用例
const result1 = validateFormInput({
  email: "   ",  // 空白のみ
  name: "",      // 空文字
});
// result1: ["メールアドレスは必須です", "名前は必須です"]

const result2 = validateFormInput({
  email: "user@example.com",
  name: "山田太郎",
  comment: "   ",  // 空白のみ
});
// result2: ["コメントは空白のみにできません"]

パターン3: 条件別の判定パターン一覧

// ケース1: null / undefined のみを判定したい
function checkNullish(value: string | null | undefined): boolean {
  return value == null;  // nullとundefinedのみtrue
}

// ケース2: null / undefined / 空文字を判定したい
function checkEmpty(value: string | null | undefined): boolean {
  return !value;  // nullとundefinedと空文字でtrue
  // または: value == null || value === ""
}

// ケース3: null / undefined / 空文字 / 空白文字のみを判定したい
function checkBlank(value: string | null | undefined): boolean {
  return !value || value.trim() === "";
  // または: value == null || value.trim() === ""
}

// 使用例
console.log(checkNullish(null));      // true
console.log(checkNullish(undefined)); // true
console.log(checkNullish(""));        // false
console.log(checkNullish("   "));     // false

console.log(checkEmpty(null));        // true
console.log(checkEmpty(undefined));   // true
console.log(checkEmpty(""));          // true
console.log(checkEmpty("   "));       // false(空白文字は含まれている)

console.log(checkBlank(null));        // true
console.log(checkBlank(undefined));   // true
console.log(checkBlank(""));          // true
console.log(checkBlank("   "));       // true(空白のみ)
console.log(checkBlank("hello"));     // false

パターン4: 配列やオブジェクトの空チェック

function checkArrayEmpty<T>(arr: T[] | null | undefined): boolean {
  return arr == null || arr.length === 0;
}

function checkObjectEmpty(obj: Record<string, unknown> | null | undefined): boolean {
  return obj == null || Object.keys(obj).length === 0;
}

// 使用例
console.log(checkArrayEmpty(null));           // true
console.log(checkArrayEmpty(undefined));      // true
console.log(checkArrayEmpty([]));             // true
console.log(checkArrayEmpty([1, 2, 3]));      // false

console.log(checkObjectEmpty(null));          // true
console.log(checkObjectEmpty({}));            // true
console.log(checkObjectEmpty({ key: "value" })); // false

実務での統合例:ユーザー登録フォーム

interface RegistrationForm {
  username: string;
  email: string;
  password: string;
  phoneNumber?: string;
  bio?: string;
}

function validateRegistration(form: RegistrationForm): {
  isValid: boolean;
  errors: Record<string, string>;
} {
  const errors: Record<string, string> = {};

  // username: 必須、3文字以上
  if (!form.username?.trim()) {
    errors.username = "ユーザー名は必須です";
  } else if (form.username.trim().length < 3) {
    errors.username = "ユーザー名は3文字以上で入力してください";
  }

  // email: 必須、形式チェック
  if (!form.email?.trim()) {
    errors.email = "メールアドレスは必須です";
  } else if (!/^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$/.test(form.email)) {
    errors.email = "有効なメールアドレスを入力してください";
  }

  // password: 必須、8文字以上
  if (!form.password) {
    errors.password = "パスワードは必須です";
  } else if (form.password.length < 8) {
    errors.password = "パスワードは8文字以上で入力してください";
  }

  // phoneNumber: 任意、値がある場合のみ検証
  if (form.phoneNumber !== undefined && form.phoneNumber.trim() === "") {
    errors.phoneNumber = "電話番号は空白のみにできません";
  }

  // bio: 任意、値がある場合は文字数制限
  if (form.bio !== undefined && form.bio.trim().length > 200) {
    errors.bio = "自己紹介は200文字以内で入力してください";
  }

  return {
    isValid: Object.keys(errors).length === 0,
    errors,
  };
}

if (value) が危険な理由|0やfalseが弾かれる落とし穴

if (value)によるシンプルな判定は便利ですが、JavaScriptのfalsy値の挙動を理解していないと、予期しないバグを引き起こします。

JavaScriptのfalsy値一覧

以下の値はすべてif文でfalseとして扱われます。

const falsyValues = [
  false,        // boolean
  0,            // number
  -0,           // number
  0n,           // bigint
  "",           // string
  null,         // null
  undefined,    // undefined
  NaN,          // number
];

falsyValues.forEach(value => {
  if (value) {
    console.log("truthy");
  } else {
    console.log("falsy");  // すべてここに来る
  }
});

実務でよくある落とし穴

// ❌ 危険な例1:数値の0が弾かれる
function displayCount(count: number | null | undefined) {
  if (!count) {
    return "データがありません";
  }
  return `件数: ${count}`;
}

console.log(displayCount(0));     // "データがありません" ← 間違い!
console.log(displayCount(5));     // "件数: 5"
console.log(displayCount(null));  // "データがありません"

// ✅ 正しい実装
function displayCountCorrect(count: number | null | undefined) {
  if (count == null) {  // nullとundefinedのみを判定
    return "データがありません";
  }
  return `件数: ${count}`;
}

console.log(displayCountCorrect(0));     // "件数: 0" ← 正しい!
console.log(displayCountCorrect(5));     // "件数: 5"
console.log(displayCountCorrect(null));  // "データがありません"

// ❌ 危険な例2:booleanのfalseが判定を誤らせる
interface UserSettings {
  notifications: boolean | null;  // null = 未設定
  darkMode: boolean | null;
}

function applySettings(settings: UserSettings) {
  // 間違った判定
  if (!settings.notifications) {
    console.log("通知設定が未設定です");  // falseの場合も「未設定」になってしまう
  }

  // 正しい判定
  if (settings.notifications === null) {
    console.log("通知設定が未設定です");
  } else if (settings.notifications) {
    console.log("通知が有効です");
  } else {
    console.log("通知が無効です");  // false の正しい処理
  }
}

applySettings({ notifications: false, darkMode: null });
// 間違った判定: "通知設定が未設定です"
// 正しい判定: "通知が無効です"

falsy判定が安全なケース

文字列の空チェックなど、0やfalseが含まれないことが型で保証されている場合は、if (value)が有効です。

// ✅ 安全な使用例:文字列のみの場合
function greet(name: string | null | undefined) {
  if (!name) {  // nameは文字列なので、0やfalseの心配がない
    return "こんにちは、ゲストさん";
  }
  return `こんにちは、${name}さん`;
}

// ✅ 安全な使用例:配列のみの場合
function processItems(items: string[] | null | undefined) {
  if (!items || items.length === 0) {
    console.log("アイテムがありません");
    return;
  }

  items.forEach(item => console.log(item));
}

判定方法の選択表

判定したい対象推奨される判定方法避けるべき方法
null / undefined のみvalue == null!value
null / undefined / 空文字!value または value == null \|\| value === ""
数値の null / undefinedvalue == null!value(0が弾かれる)
boolean の null / undefinedvalue === null!value(falseが弾かれる)
配列の空arr == null \|\| arr.length === 0!arr
オブジェクトの空obj == null \|\| Object.keys(obj).length === 0!obj

Boolean(value) や !!value を使うときにハマりやすい落とし穴と回避策

Boolean(value)や二重否定!!valueは、値を明示的にboolean型に変換する手法ですが、これもfalsy値の挙動を理解していないと危険です。

Boolean() と !! の基本

// これらは完全に同じ挙動
console.log(Boolean(0));      // false
console.log(!!0);             // false

console.log(Boolean("hello")); // true
console.log(!!"hello");        // true

console.log(Boolean(null));    // false
console.log(!!null);           // false

落とし穴1:数値の0や空文字が意図せずfalseになる

interface Product {
  price: number | null;
  stock: number | null;
}

// ❌ 危険なコード
function isProductAvailable(product: Product): boolean {
  return !!product.price && !!product.stock;
}

const freeItem = { price: 0, stock: 10 };      // 無料商品
console.log(isProductAvailable(freeItem));     // false ← 間違い!

const outOfStock = { price: 1000, stock: 0 };  // 在庫切れ
console.log(isProductAvailable(outOfStock));   // false ← これは正しい

// ✅ 正しいコード
function isProductAvailableCorrect(product: Product): boolean {
  return product.price != null && product.stock != null && product.stock > 0;
}

console.log(isProductAvailableCorrect(freeItem));     // true ← 正しい!
console.log(isProductAvailableCorrect(outOfStock));   // false

落とし穴2:配列のフィルタリングで意図しない除外が発生

// ❌ 危険な例:0やfalseが除外されてしまう
const mixedArray = [1, 0, 2, false, 3, "", 4, null, 5, undefined];
const filtered = mixedArray.filter(item => !!item);
console.log(filtered);  // [1, 2, 3, 4, 5] ← 0とfalseと空文字も除外された

// ✅ 正しい例:nullとundefinedのみ除外
const filteredCorrect = mixedArray.filter(item => item != null);
console.log(filteredCorrect);  // [1, 0, 2, false, 3, "", 4, 5] ← 0とfalseは残る

落とし穴3:型ガードとして機能しない場合がある

function processValue(value: string | number | null | undefined) {
  if (!!value) {
    // valueの型は string | number | false のまま
    // nullとundefinedは除外されているが、0や空文字の可能性も残る
    console.log(value.toString());  // 型エラーにはならないが、0の場合は"0"になる
  }
}

// より明確な型ガード
function processValueCorrect(value: string | number | null | undefined) {
  if (value != null) {
    // valueの型は string | number に絞り込まれる
    console.log(value.toString());  // 安全に使用できる
  }
}

!! を安全に使えるケース

// ✅ 使用例1:明示的なboolean変換が必要な場合
interface User {
  name: string;
  isAdmin: boolean | undefined;
}

function getUserRole(user: User): string {
  // isAdminをbooleanに変換(undefinedをfalseとして扱う)
  const isAdmin = !!user.isAdmin;
  return isAdmin ? "管理者" : "一般ユーザー";
}

// ✅ 使用例2:文字列の存在チェック(0やfalseが含まれない)
function hasContent(text: string | null | undefined): boolean {
  return !!text;  // nullやundefinedや空文字をfalseに変換
}

// ✅ 使用例3:配列の要素数チェック
function hasItems<T>(items: T[] | null | undefined): boolean {
  return !!(items && items.length);
}

実践的な回避策

// パターン1:型ごとに明示的に判定する
function validateInput(input: {
  count: number | null;
  enabled: boolean | null;
  message: string | null;
}) {
  // 数値は == null で判定
  if (input.count == null) {
    console.log("件数が未設定です");
  } else if (input.count === 0) {
    console.log("件数が0です");
  }

  // booleanは === null で判定
  if (input.enabled === null) {
    console.log("有効/無効が未設定です");
  } else if (input.enabled) {
    console.log("有効です");
  } else {
    console.log("無効です");
  }

  // 文字列は !value で判定可能(0やfalseの心配がない)
  if (!input.message) {
    console.log("メッセージが空です");
  }
}

// パターン2:ヘルパー関数を作成する
function isNullish(value: unknown): value is null | undefined {
  return value == null;
}

function isEmpty(value: string | null | undefined): boolean {
  return value == null || value === "";
}

function isValidNumber(value: number | null | undefined): value is number {
  return value != null && !Number.isNaN(value);
}

// 使用例
const userInput = {
  age: 0,
  name: "",
  email: null,
};

console.log(isNullish(userInput.age));        // false(0は有効な値)
console.log(isEmpty(userInput.name));         // true
console.log(isValidNumber(userInput.age));    // true(0は有効な数値)

まとめ:安全な判定のためのチェックリスト

// ✅ チェックリスト
// 1. 判定対象の型を確認する
// 2. 0やfalseが有効な値として含まれるか考える
// 3. null/undefinedのみ弾きたいなら == null
// 4. 空文字も弾きたいなら !value(文字列型の場合のみ安全)
// 5. 数値やbooleanには !! を使わない
// 6. 迷ったら明示的に === null や === undefined を使う

// 実践的な統合例
interface FormData {
  quantity: number | null;        // 数量(0も有効)
  acceptTerms: boolean | null;    // 規約同意(falseも有効)
  comment: string | null;         // コメント(空文字は無効)
}

function validateForm(data: FormData): string[] {
  const errors: string[] = [];

  // 数値:0も有効なので == null で判定
  if (data.quantity == null) {
    errors.push("数量を入力してください");
  } else if (data.quantity < 0) {
    errors.push("数量は0以上で入力してください");
  }

  // boolean:falseも有効なので === null で判定
  if (data.acceptTerms === null) {
    errors.push("規約への同意が必要です");
  } else if (!data.acceptTerms) {
    errors.push("規約に同意してください");
  }

  // 文字列:空文字も無効なので !value で判定可能
  if (!data.comment || data.comment.trim() === "") {
    errors.push("コメントを入力してください");
  }

  return errors;
}

これらのパターンを理解し、状況に応じて適切な判定方法を選択することで、バグのない堅牢なコードを書けるようになります。

◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆

TypeScriptの新常識!Optional Chaining(?.)とNullish Coalescing(??)

Optional Chaining(?.)でundefinedアクセスを安全に回避

Optional Chaining(?.)は、ES2020で導入され、TypeScript 3.7以降で利用可能になったモダンな構文です。ネストしたオブジェクトのプロパティアクセスを安全に行えます。

従来の冗長な書き方との比較

interface User {
  name: string;
  address?: {
    city: string;
    postalCode?: string;
  };
}

const user: User | null = getUser();

// ❌ Before: 冗長なnullチェック
let city: string | undefined;
if (user !== null && user.address !== undefined) {
  city = user.address.city;
}

// ✅ After: Optional Chainingで簡潔に
const city2 = user?.address?.city;

基本的な使い方

// プロパティアクセス
const userName = user?.name;  // userがnullやundefinedならundefinedを返す

// ネストしたプロパティ
const postalCode = user?.address?.postalCode;

// 配列の要素アクセス
const firstItem = items?.[0];

// メソッド呼び出し
const result = obj?.method?.();

// 関数呼び出し(関数自体がnullの可能性がある場合)
const callback: (() => void) | null = getCallback();
callback?.();  // callbackがnullならば何もしない

実務での活用例1:APIレスポンスの処理

interface APIResponse {
  data?: {
    user?: {
      profile?: {
        avatar?: string;
        bio?: string;
      };
      settings?: {
        notifications?: boolean;
      };
    };
  };
}

function displayUserInfo(response: APIResponse) {
  // ❌ 従来の書き方:各階層でチェックが必要
  let avatar: string | undefined;
  if (response.data &&
      response.data.user &&
      response.data.user.profile) {
    avatar = response.data.user.profile.avatar;
  }

  // ✅ Optional Chainingで簡潔に
  const avatar2 = response.data?.user?.profile?.avatar;
  const bio = response.data?.user?.profile?.bio;
  const notifications = response.data?.user?.settings?.notifications;

  console.log(avatar2 ?? "デフォルトアバター");
  console.log(bio ?? "自己紹介なし");
  console.log(notifications ?? true);  // デフォルトはtrue
}

実務での活用例2:イベントハンドラー

// DOMイベントの処理
function handleClick(event: MouseEvent) {
  // クリックされた要素のdata属性を安全に取得
  const userId = (event.target as HTMLElement)?.dataset?.userId;

  if (userId) {
    loadUserProfile(userId);
  }
}

// フォームのバリデーション
function validateForm(formElement: HTMLFormElement | null) {
  // フォーム要素が存在する場合のみバリデーションを実行
  const isValid = formElement?.checkValidity?.() ?? false;

  if (!isValid) {
    formElement?.reportValidity?.();
  }

  return isValid;
}

実務での活用例3:配列操作

interface Product {
  name: string;
  reviews?: {
    rating: number;
    comment: string;
  }[];
}

function getFirstReviewRating(product: Product | null): number | undefined {
  // 配列の最初の要素に安全にアクセス
  return product?.reviews?.[0]?.rating;
}

function getAverageRating(product: Product | null): number {
  const reviews = product?.reviews;

  if (!reviews || reviews.length === 0) {
    return 0;
  }

  const sum = reviews.reduce((acc, review) => acc + review.rating, 0);
  return sum / reviews.length;
}

// 使用例
const product: Product | null = {
  name: "ノートPC",
  reviews: [
    { rating: 5, comment: "素晴らしい" },
    { rating: 4, comment: "良い" },
  ],
};

console.log(getFirstReviewRating(product));  // 5
console.log(getFirstReviewRating(null));     // undefined

Optional Chainingの動作詳細

// Optional Chainingは以下のように展開される
obj?.prop
// ↓ 実際の動作
(obj === null || obj === undefined) ? undefined : obj.prop

obj?.method()
// ↓ 実際の動作
(obj === null || obj === undefined) ? undefined : obj.method()

arr?.[index]
// ↓ 実際の動作
(arr === null || arr === undefined) ? undefined : arr[index]

注意点:Optional Chainingが評価を中断するタイミング

const obj = {
  a: {
    b: 0,  // 0はfalsyだがnullやundefinedではない
  },
};

console.log(obj?.a?.b);  // 0(undefinedにならない)

const obj2 = {
  a: null,
};

console.log(obj2?.a?.b);  // undefined(aがnullなので以降の評価が中断される)

Nullish Coalescing(??)と論理OR(||)の決定的な違い

Nullish Coalescing(??)は、ES2020で導入された演算子で、nullまたはundefinedの場合のみデフォルト値を返します。従来の論理OR(||)との違いを理解することが重要です。

|| と ?? の挙動の違い

// || はすべてのfalsyな値でデフォルト値を返す
console.log(0 || 10);        // 10
console.log("" || "default"); // "default"
console.log(false || true);  // true
console.log(null || "default"); // "default"

// ?? はnullとundefinedの場合のみデフォルト値を返す
console.log(0 ?? 10);        // 0
console.log("" ?? "default"); // ""
console.log(false ?? true);  // false
console.log(null ?? "default"); // "default"

実務でよくあるバグと解決策

interface Config {
  port: number | null;
  retryCount: number | null;
  enableCache: boolean | null;
}

function loadConfig(config: Config) {
  // ❌ || を使った場合:0やfalseが意図せずデフォルト値になる
  const port = config.port || 3000;
  const retryCount = config.retryCount || 3;
  const enableCache = config.enableCache || false;

  console.log(port);        // config.portが0の場合、3000になってしまう
  console.log(retryCount);  // config.retryCountが0の場合、3になってしまう
  console.log(enableCache); // config.enableCacheがfalseの場合も常にfalse

  // ✅ ?? を使った場合:nullとundefinedの場合のみデフォルト値
  const port2 = config.port ?? 3000;
  const retryCount2 = config.retryCount ?? 3;
  const enableCache2 = config.enableCache ?? false;

  console.log(port2);        // config.portが0でも0が使われる
  console.log(retryCount2);  // config.retryCountが0でも0が使われる
  console.log(enableCache2); // config.enableCacheがfalseならfalseが使われる
}

// 使用例
loadConfig({
  port: 0,           // 0は有効な値
  retryCount: 0,     // リトライしない設定
  enableCache: false, // キャッシュを無効化
});

実践的な使用例

// 例1:ユーザー設定のデフォルト値
interface UserSettings {
  theme?: "light" | "dark";
  fontSize?: number;
  showNotifications?: boolean;
}

function applyUserSettings(settings: UserSettings) {
  // ?? を使うことで、設定が明示的にundefinedの場合のみデフォルト値を適用
  const theme = settings.theme ?? "light";
  const fontSize = settings.fontSize ?? 16;
  const showNotifications = settings.showNotifications ?? true;

  console.log(`テーマ: ${theme}, フォントサイズ: ${fontSize}px, 通知: ${showNotifications}`);
}

applyUserSettings({});
// テーマ: light, フォントサイズ: 16px, 通知: true

applyUserSettings({ theme: "dark", fontSize: 0, showNotifications: false });
// テーマ: dark, フォントサイズ: 0px, 通知: false(0とfalseが正しく保持される)

// 例2:検索パラメータの処理
interface SearchParams {
  query: string;
  page?: number;
  limit?: number;
  sortBy?: string;
}

function buildSearchQuery(params: SearchParams): string {
  const page = params.page ?? 1;
  const limit = params.limit ?? 20;
  const sortBy = params.sortBy ?? "createdAt";

  return `?query=${params.query}&page=${page}&limit=${limit}&sortBy=${sortBy}`;
}

console.log(buildSearchQuery({ query: "TypeScript" }));
// ?query=TypeScript&page=1&limit=20&sortBy=createdAt

console.log(buildSearchQuery({ query: "TypeScript", page: 0, limit: 0 }));
// ?query=TypeScript&page=0&limit=0&sortBy=createdAt(0が正しく保持される)

|| と ?? の使い分け一覧表

シナリオ推奨される演算子理由
数値(0も有効)??0がデフォルト値に置き換わらない
boolean(falseも有効)??falseがデフォルト値に置き換わらない
文字列(空文字も無効としたい)`
文字列(空文字も有効)??空文字が保持される
オブジェクト・配列??一貫性のため
// 実践的な使い分け例
interface FormInput {
  username: string;      // 空文字は無効
  age: number | null;    // 0も有効な値
  agreeToTerms: boolean | null; // falseも有効な値
}

function processForm(input: FormInput) {
  // usernameは空文字を無効としたいので || を使用
  const username = input.username || "ゲスト";

  // ageは0も有効なので ?? を使用
  const age = input.age ?? 18;

  // agreeToTermsはfalseも有効なので ?? を使用
  const agreeToTerms = input.agreeToTerms ?? false;

  return { username, age, agreeToTerms };
}

console.log(processForm({ username: "", age: 0, agreeToTerms: false }));
// { username: "ゲスト", age: 0, agreeToTerms: false }

最新TypeScriptで推奨される簡潔で可読性の高い判定構文

Optional ChainingとNullish Coalescingを組み合わせることで、極めて簡潔で安全なコードが書けます。

パターン1:?. と ?? の組み合わせ

interface User {
  name: string;
  profile?: {
    avatar?: string;
    bio?: string;
  };
}

// ❌ 従来の冗長な書き方
function getUserAvatar(user: User | null | undefined): string {
  let avatar: string;

  if (user && user.profile && user.profile.avatar) {
    avatar = user.profile.avatar;
  } else {
    avatar = "/default-avatar.png";
  }

  return avatar;
}

// ✅ モダンな書き方
function getUserAvatarModern(user: User | null | undefined): string {
  return user?.profile?.avatar ?? "/default-avatar.png";
}

パターン2:配列処理との組み合わせ

interface BlogPost {
  title: string;
  author?: {
    name: string;
    posts?: BlogPost[];
  };
}

function getAuthorPostCount(post: BlogPost | null): number {
  // ❌ 従来の書き方
  if (post && post.author && post.author.posts) {
    return post.author.posts.length;
  }
  return 0;

  // ✅ モダンな書き方
  return post?.author?.posts?.length ?? 0;
}

function getFirstPostTitle(post: BlogPost | null): string {
  return post?.author?.posts?.[0]?.title ?? "投稿なし";
}

パターン3:関数呼び出しとの組み合わせ

interface API {
  users?: {
    getById?: (id: string) => Promise<User | null>;
    getAll?: () => Promise<User[]>;
  };
}

async function fetchUser(api: API | null, userId: string): Promise<User | null> {
  // ❌ 従来の書き方
  if (api && api.users && api.users.getById) {
    return await api.users.getById(userId);
  }
  return null;

  // ✅ モダンな書き方
  return await api?.users?.getById?.(userId) ?? null;
}

パターン4:複雑なネスト構造の安全なアクセス

interface Company {
  departments?: {
    engineering?: {
      teams?: {
        name: string;
        members?: {
          name: string;
          email: string;
        }[];
      }[];
    };
  };
}

function getFirstEngineerEmail(company: Company | null): string | undefined {
  // ❌ 従来の書き方(15行以上)
  if (!company) return undefined;
  if (!company.departments) return undefined;
  if (!company.departments.engineering) return undefined;
  if (!company.departments.engineering.teams) return undefined;
  if (company.departments.engineering.teams.length === 0) return undefined;
  const firstTeam = company.departments.engineering.teams[0];
  if (!firstTeam.members) return undefined;
  if (firstTeam.members.length === 0) return undefined;
  return firstTeam.members[0].email;

  // ✅ モダンな書き方(1行)
  return company?.departments?.engineering?.teams?.[0]?.members?.[0]?.email;
}

実務での総合例:Next.jsでのデータフェッチング

interface PageProps {
  data?: {
    post?: {
      title: string;
      content: string;
      author?: {
        name: string;
        avatar?: string;
      };
      tags?: string[];
      relatedPosts?: {
        id: string;
        title: string;
      }[];
    };
  };
}

export default function BlogPostPage({ data }: PageProps) {
  // すべてのプロパティに安全にアクセス
  const title = data?.post?.title ?? "タイトルなし";
  const content = data?.post?.content ?? "";
  const authorName = data?.post?.author?.name ?? "匿名";
  const authorAvatar = data?.post?.author?.avatar ?? "/default-avatar.png";
  const tags = data?.post?.tags ?? [];
  const relatedCount = data?.post?.relatedPosts?.length ?? 0;
  const firstRelated = data?.post?.relatedPosts?.[0]?.title;

  return (
    <article>
      <h1>{title}</h1>
      <div className="author">
        <img src={authorAvatar} alt={authorName} />
        <span>{authorName}</span>
      </div>
      <div dangerouslySetInnerHTML={{ __html: content }} />
      <div className="tags">
        {tags.map(tag => <span key={tag}>#{tag}</span>)}
      </div>
      {relatedCount > 0 && (
        <aside>
          <h2>関連記事({relatedCount}件)</h2>
          {firstRelated && <p>最新: {firstRelated}</p>}
        </aside>
      )}
    </article>
  );
}

モダンな構文を使う際のベストプラクティス

// ✅ Good: 簡潔で可読性が高い
const email = user?.contact?.email ?? "メールアドレス未登録";

// ✅ Good: チェーンが長すぎる場合は中間変数を使う
const contact = user?.contact;
const email2 = contact?.email ?? contact?.phone ?? "連絡先未登録";

// ⚠️ 注意: チェーンが極端に長い場合は可読性が下がる
const value = obj?.a?.b?.c?.d?.e?.f?.g?.h?.i?.j;  // 長すぎる

// ✅ Better: 適度に分割する
const intermediate = obj?.a?.b?.c?.d;
const value2 = intermediate?.e?.f?.g?.h?.i?.j;

// ✅ Good: デフォルト値が複雑な場合は関数化
const getUserDisplayName = (user: User | null) => {
  return user?.profile?.displayName
    ?? user?.profile?.firstName
    ?? user?.email?.split("@")[0]
    ?? "ゲストユーザー";
};

型安全性を保ちながら簡潔に書く

// Optional ChainingとNullish Coalescingは型推論と相性が良い
interface Response {
  data?: {
    items?: string[];
  };
}

function processResponse(response: Response) {
  // itemsの型は string[] | undefined と推論される
  const items = response.data?.items;

  if (items) {
    // この時点でitemsは string[] 型
    items.forEach(item => console.log(item.toUpperCase()));
  }

  // デフォルト値を設定すると型が確定する
  const safeItems = response.data?.items ?? [];  // 型は string[]
  safeItems.forEach(item => console.log(item));  // 型エラーなし
}

これらのモダンな構文を活用することで、コードの行数を削減しながら、型安全性と可読性を両立できます。TypeScript 3.7以降を使用している場合は、積極的に活用することを推奨します。

国内シェアNo.1のエックスサーバーが提供するVPSサーバー『XServer VPS』

よくある質問

Non-null assertion operator (!) はいつ使うべきか?

Non-null assertion operator(!)は、TypeScriptコンパイラに対して「この値は絶対にnullやundefinedではない」と明示的に伝える演算子です。しかし、使用は極力避けるべきです。

基本的な使い方

function getElement(id: string): HTMLElement | null {
  return document.getElementById(id);
}

// ❌ Non-null assertionを使った例
const element = getElement("myButton")!;
element.addEventListener("click", handleClick);  // elementがnullならランタイムエラー

// ✅ 推奨される書き方
const element2 = getElement("myButton");
if (element2) {
  element2.addEventListener("click", handleClick);
}

// ✅ Optional Chainingを使った書き方
getElement("myButton")?.addEventListener("click", handleClick);

! を使うことの問題点

interface User {
  name: string;
  email: string;
}

let currentUser: User | null = null;

// ❌ 危険:実行時エラーになる
function displayUserName() {
  console.log(currentUser!.name);  // currentUserがnullの場合エラー
}

// ✅ 安全:nullチェックを行う
function displayUserNameSafe() {
  if (currentUser === null) {
    console.log("ユーザーが未設定です");
    return;
  }
  console.log(currentUser.name);
}

例外的に ! を使っても良いケース

すぐ上の行でnullチェックをしているが、TypeScriptの型推論が効かない場合

const items: string[] | null = getItems();

if (items === null) {
  return;
}

// この時点でitemsはnullではないが、別の関数内では型が絞り込まれない
setTimeout(() => {
  // ❌ TypeScriptはitemsがnullの可能性があると判断する
  // console.log(items.length);  // Error

  // ✅ この場合のみ ! が許容される(ただし非推奨)
  console.log(items!.length);

  // ✅ より良い方法:変数を再代入
  const safeItems = items;
  setTimeout(() => {
    console.log(safeItems.length);  // 型推論が効く
  }, 100);
}, 100);

外部ライブラリの型定義が不正確で、確実に値が存在することが保証されている場合

// 例:ReactのuseRefで、必ず値が設定されることが分かっている場合
import { useRef, useEffect } from "react";

function Component() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // ❌ あまり良くない
    inputRef.current!.focus();

    // ✅ より安全
    if (inputRef.current) {
      inputRef.current.focus();
    }

    // ✅ さらに良い:Optional Chaining
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

テストコードなど、エラーが起きても問題ない文脈

// テストコード内では許容されることがある
describe("User Service", () => {
  it("should return user by id", () => {
    const user = userService.findById("123")!;  // テスト内では明示的にエラーにしたい
    expect(user.name).toBe("山田太郎");
  });
});

! の代替手段

// パターン1:型ガードを使う
function assertNonNull<T>(value: T | null | undefined, message?: string): asserts value is T {
if (value == null) {
throw new Error(message || "Unexpected null or undefined");
}
}

const element = getElement("myButton");
assertNonNull(element, "Button element not found");
element.addEventListener("click", handleClick); // 型推論が効く

// パターン2:デフォルト値を設定する
const element2 = getElement("myButton") ?? document.createElement("button");
element2.addEventListener("click", handleClick); // 常に安全

// パターン3:early returnを使う
function handleButtonClick() {
const element = getElement("myButton");
if (!element) {
console.error("Button not found");
return;
}

// この時点でelementはHTMLElement型
element.addEventListener("click", handleClick);
}

型アサーション(as)とNon-null assertion(!)の違いは?

型アサーション(as)とNon-null assertion(!)は、どちらもTypeScriptコンパイラの型チェックを「上書き」する機能ですが、用途と危険性が異なります。

型アサーション(as)の基本

// 型アサーションは「この値をこの型として扱ってください」と指示する
const input = document.getElementById("username") as HTMLInputElement;
input.value = "test";  // HTMLInputElementとして扱われる

// 同じ意味(古い構文)
const input2 = <HTMLInputElement>document.getElementById("username");

Non-null assertion(!)の基本

// Non-null assertionは「この値はnullやundefinedではない」と指示する
const input = document.getElementById("username")!;
input.addEventListener("click", handleClick);  // null チェックなしで使える

違いの比較表

項目型アサーション(as)Non-null assertion(!)
用途型を別の型に変換nullやundefinedを除外
構文value as Typevalue!
実行時の影響なしなし
危険性高い(間違った型を指定できる)高い(実際にnullの可能性がある)
推奨度限定的に使用極力避ける

実務での使い分け

// ケース1:DOM要素の型アサーション
// ✅ 適切な使用例:特定の型であることが確実な場合
const form = document.querySelector("form") as HTMLFormElement;
const input = form.querySelector('input[name="email"]') as HTMLInputElement;

// ❌ 危険な例:型が一致しない可能性がある
const button = document.querySelector(".btn") as HTMLButtonElement;
// .btnがdivやspanの場合、実行時エラーになる

// ✅ より安全:型ガードを使う
const element = document.querySelector(".btn");
if (element instanceof HTMLButtonElement) {
  element.disabled = true;
}

// ケース2:APIレスポンスの型アサーション
interface User {
  id: string;
  name: string;
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // ❌ 危険:dataの構造が保証されていない
  return data as User;

  // ✅ より安全:バリデーションを行う
  if (typeof data.id === "string" && typeof data.name === "string") {
    return data as User;
  }
  throw new Error("Invalid user data");
}

// ✅ さらに安全:zodなどのバリデーションライブラリを使う
import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

async function fetchUserSafe(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return UserSchema.parse(data);  // バリデーションエラー時は例外を投げる
}

asとの組み合わせ

// as と ! を組み合わせることもできる(非推奨)
const input = document.querySelector(".input") as HTMLInputElement | null;
const value = input!.value;  // inputがnullの可能性を無視

// ✅ より安全な書き方
const input2 = document.querySelector(".input");
if (input2 instanceof HTMLInputElement) {
  const value = input2.value;
}

安全な代替手段

// パターン1:ユーザー定義型ガード
function isHTMLInputElement(element: Element | null): element is HTMLInputElement {
return element instanceof HTMLInputElement;
}

const element = document.querySelector(".input");
if (isHTMLInputElement(element)) {
console.log(element.value); // 型安全
}

// パターン2:型述語を使った汎用ヘルパー
function assertType<T>(value: unknown, validator: (v: unknown) => v is T): T {
if (!validator(value)) {
throw new Error("Type assertion failed");
}
return value;
}

const input3 = assertType(
document.querySelector(".input"),
isHTMLInputElement
);

undefined と null、どちらを使うべきか?

実務では、undefinednullのどちらを使うべきか迷うことがあります。以下のガイドラインを参考にしてください。

基本的な使い分けの指針

// ✅ undefinedを使う場合:オプショナルなプロパティ
interface User {
  name: string;
  email: string;
  phoneNumber?: string;  // 省略可能な場合はundefined
  avatar?: string;
}

// ✅ nullを使う場合:明示的に「値がない」ことを表現したい
interface APIResponse {
  user: User | null;  // ユーザーが見つからない場合はnull
  error: string | null;  // エラーがない場合はnull
}

それぞれの特性と使い分け

項目undefinednull
意味未定義、初期化されていない明示的に「値がない」
typeof"undefined""object"
JSONでの扱いプロパティが省略されるnullとして保持される
関数の戻り値デフォルトでundefined明示的に返す必要がある
推奨される用途オプショナルプロパティ、デフォルト引数APIレスポンス、明示的な「なし」

実務での使い分け例

// ケース1:関数の戻り値
// ✅ undefinedを返す:「見つからなかった」
function findUser(id: string): User | undefined {
  const user = users.find(u => u.id === id);
  return user;  // 見つからない場合は自然にundefinedになる
}

// ✅ nullを返す:「存在しない」ことを明示
function getUserById(id: string): User | null {
  const user = database.query(`SELECT * FROM users WHERE id = ?`, [id]);
  return user || null;  // 明示的にnullを返す
}

// ケース2:状態管理
interface AppState {
  currentUser: User | null;     // ログインしていない場合はnull
  selectedItem?: Item;          // 何も選択していない場合はundefined
  error: Error | null;          // エラーがない場合はnull
  loadingMessage?: string;      // ローディング中でない場合はundefined
}

// ケース3:APIレスポンス
// ✅ JSONでの扱いを考慮してnullを使う
interface ProductResponse {
  product: {
    id: string;
    name: string;
    description: string | null;  // 説明がない場合はnull
    discount: number | null;     // 割引なしの場合はnull
  } | null;  // 商品が見つからない場合はnull
}

// JSONシリアライズ時の違い
const data1 = { name: "太郎", age: undefined };
console.log(JSON.stringify(data1));  // {"name":"太郎"} (ageは省略される)

const data2 = { name: "太郎", age: null };
console.log(JSON.stringify(data2));  // {"name":"太郎","age":null} (nullは保持される)

チーム開発でのベストプラクティス

// プロジェクト全体で統一されたルールを決める例

// ルール1:関数の戻り値は undefined を優先
function getConfig(key: string): string | undefined {
  return config[key];
}

// ルール2:APIレスポンスは null を優先
interface ApiUser {
  id: string;
  name: string;
  deletedAt: Date | null;  // 削除されていない場合はnull
}

// ルール3:Reactのステートは null を優先
function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<Error | null>(null);

  // ...
}

// ルール4:オプショナルプロパティは ? (undefined) を使う
interface FormData {
  email: string;
  name: string;
  phoneNumber?: string;  // 任意項目
  bio?: string;
}

nullとundefinedを統一して扱う

// どちらを使っても良い場合は、統一して扱う
function isEmpty(value: string | null | undefined): boolean {
return value == null || value === "";
}

// デフォルト値を設定する場合は ?? を使う
function getDisplayName(name: string | null | undefined): string {
return name ?? "匿名ユーザー";
}

// 型定義で両方を許容する
type Nullable<T> = T | null | undefined;

function processValue(value: Nullable<string>) {
if (value == null) {
return "値なし";
}
return value.toUpperCase();
}

strictNullChecks を既存プロジェクトで有効化する方法は?

既存の大規模プロジェクトでstrictNullChecksを有効化するのは困難ですが、段階的に移行する方法があります。

段階的な移行戦略

// ステップ1:tsconfig.json で段階的な有効化を設定
{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": false,
    // まずは新しいファイルのみ厳格にチェック
  },
  "include": ["src/**/*"],
  "exclude": ["src/legacy/**/*"]  // レガシーコードは除外
}

// ステップ2:新規ファイルから厳格にチェック
// 新しいファイルの先頭に以下を追加
// @ts-check
// または各ファイルで
/// <reference types="typescript" />

ファイル単位での移行

// 移行前のコード(strictNullChecks: false)
function getUserName(user: User) {
  return user.name;  // userがnullでもエラーにならない
}

// 移行後のコード(strictNullChecks: true)
function getUserName(user: User | null): string {
  if (user === null) {
    return "ゲスト";
  }
  return user.name;
}

// または
function getUserNameSafe(user: User | null): string {
  return user?.name ?? "ゲスト";
}

実践的な移行手順

// 1. ユーティリティ関数を作成
function assertNonNull<T>(value: T | null | undefined, name: string): T {
  if (value == null) {
    throw new Error(`${name} is null or undefined`);
  }
  return value;
}

function orDefault<T>(value: T | null | undefined, defaultValue: T): T {
  return value ?? defaultValue;
}

// 2. 既存コードを徐々に書き換え
// Before
const user = getUser();
console.log(user.name);

// After(段階1:assertionを使う)
const user = assertNonNull(getUser(), "user");
console.log(user.name);

// After(段階2:型定義を修正)
const user = getUser();  // User | null を返すように修正
if (user) {
  console.log(user.name);
}

// 3. 型定義ファイルを整備
// types/legacy.d.ts
declare module "legacy-module" {
  export function getLegacyData(): any;  // 一旦anyで逃げる
}

// 徐々に厳密な型に置き換えていく
declare module "legacy-module" {
  export interface LegacyData {
    id: string;
    name: string | null;
  }
  export function getLegacyData(): LegacyData | null;
}

移行時の注意点とESLintルール

// .eslintrc.json
{
"rules": {
// 移行期間中は警告レベルに下げる
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/strict-boolean-expressions": "off",

// 徐々に厳格にしていく
"@typescript-eslint/no-unnecessary-condition": "warn"
}
}

Map や Set でnull/undefinedを扱う際の注意点は?

MapSetでnull/undefinedを扱う際は、通常のオブジェクトとは異なる挙動に注意が必要です。

// Mapの基本的な挙動
const map = new Map<string, string | null>();

map.set("key1", "value1");
map.set("key2", null);
map.set("key3", undefined);  // undefinedも設定できる

console.log(map.has("key1"));  // true
console.log(map.has("key2"));  // true(nullも含まれる)
console.log(map.has("key3"));  // true(undefinedも含まれる)
console.log(map.has("key4"));  // false(存在しないキー)

console.log(map.get("key1"));  // "value1"
console.log(map.get("key2"));  // null
console.log(map.get("key3"));  // undefined
console.log(map.get("key4"));  // undefined(存在しないキー)

// ⚠️ 問題:key3とkey4の区別ができない

安全な判定方法

// ✅ hasメソッドで存在確認してからgetする
function getMapValue<K, V>(map: Map<K, V>, key: K): V | undefined {
  if (map.has(key)) {
    return map.get(key);
  }
  return undefined;
}

// ✅ nullとundefinedを区別する
function getMapValueOrNull<K, V>(map: Map<K, V | null>, key: K): V | null {
  if (!map.has(key)) {
    return null;  // キーが存在しない
  }
  return map.get(key)!;  // ここではnon-null assertionが安全
}

// 実用例
const userCache = new Map<string, User | null>();

function getCachedUser(id: string): User | null | undefined {
  if (!userCache.has(id)) {
    return undefined;  // キャッシュなし
  }
  return userCache.get(id)!;  // null(ユーザー不在)またはUser
}

// 使用例
const user = getCachedUser("123");
if (user === undefined) {
  console.log("キャッシュにありません。APIから取得します");
} else if (user === null) {
  console.log("ユーザーは存在しません");
} else {
  console.log(`ユーザー名: ${user.name}`);
}

Setでのnull/undefined

// Setはnull/undefinedを値として持てる
const set = new Set<string | null | undefined>();

set.add("value");
set.add(null);
set.add(undefined);

console.log(set.has("value")); // true
console.log(set.has(null)); // true
console.log(set.has(undefined)); // true
console.log(set.size); // 3

// 実用例:入力値のユニークチェック
function getUniqueValues(items: (string | null)[]): Set<string | null> {
return new Set(items);
}

const items = ["a", "b", "a", null, "c", null];
const unique = getUniqueValues(items);
console.log(unique); // Set { "a", "b", null, "c" }

月額99円から。容量最大1TB!ブログ作成におすすめのWordPressテーマ「Cocoon」も簡単インストール

まとめ

TypeScriptにおけるnullとundefinedの判定は、一見シンプルに見えて実は奥が深く、適切な判定方法を選択することがバグのない堅牢なコードを書く上で極めて重要です。この記事で解説した内容を振り返り、実務で即座に活用できるポイントをまとめます。

基本原則:判定方法の選択フローチャート

実務では、以下のフローチャートに従って判定方法を選択することで、迷いなく適切なコードが書けます。

// 1. nullとundefinedを区別する必要があるか?
// → YES: === null または === undefined を使う
// → NO: 次へ

// 2. 空文字列も「値がない」として扱いたいか?
// → YES: !value を使う(ただし数値・booleanでないことを確認)
// → NO: value == null を使う

// 3. デフォルト値を設定したいか?
// → YES: ?? を使う(0やfalseも有効な値として扱う)
// → または || を使う(空文字をデフォルト値に置き換えたい場合)

// 4. ネストしたプロパティにアクセスしたいか?
// → YES: ?. を使う

実務で頻出するパターン別ベストプラクティス

パターン1:APIレスポンスの処理

interface APIResponse {
  data: User | null;
  error?: string;
}

function handleResponse(response: APIResponse) {
  // nullチェックには == null が簡潔
  if (response.data == null) {
    console.error(response.error ?? "不明なエラー");
    return;
  }

  // この時点でresponse.dataはUser型
  displayUser(response.data);
}

パターン2:フォームバリデーション

function validateEmail(email: string | null | undefined): boolean {
  // 空文字も無効としたい場合は !email
  if (!email || email.trim() === "") {
    return false;
  }

  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
}

パターン3:設定値のデフォルト処理

interface Config {
  port?: number;
  timeout?: number;
  retryCount?: number;
}

function loadConfig(config: Config) {
  // 0も有効な値なので ?? を使う
  const port = config.port ?? 3000;
  const timeout = config.timeout ?? 5000;
  const retryCount = config.retryCount ?? 3;

  return { port, timeout, retryCount };
}

パターン4:ネストしたオブジェクトへの安全なアクセス

interface Response {
  user?: {
    profile?: {
      avatar?: string;
    };
  };
}

function getAvatar(response: Response): string {
  // ?. と ?? を組み合わせる
  return response.user?.profile?.avatar ?? "/default-avatar.png";
}

避けるべきアンチパターン

実務でよく見かける問題のあるコードと、その改善方法をまとめます。

// ❌ アンチパターン1: 数値の判定で !value を使う
function displayCount(count: number | null) {
  if (!count) {  // 0がfalseになる
    return "データなし";
  }
  return `${count}件`;
}

// ✅ 改善版
function displayCount(count: number | null) {
  if (count == null) {
    return "データなし";
  }
  return `${count}件`;
}

// ❌ アンチパターン2: Non-null assertionの乱用
const element = document.getElementById("button")!;
element.addEventListener("click", handleClick);

// ✅ 改善版
const element = document.getElementById("button");
element?.addEventListener("click", handleClick);

// ❌ アンチパターン3: デフォルト値の設定で || を使う
const config = {
  retryCount: userConfig.retryCount || 3,  // 0が3になってしまう
};

// ✅ 改善版
const config = {
  retryCount: userConfig.retryCount ?? 3,
};

// ❌ アンチパターン4: 冗長なnullチェック
if (user !== null && user !== undefined) {
  console.log(user.name);
}

// ✅ 改善版
if (user != null) {
  console.log(user.name);
}

// ❌ アンチパターン5: typeof を不必要に使う
if (typeof value === "undefined") {
  // 通常の変数やプロパティには不要
}

// ✅ 改善版
if (value === undefined) {
  // これで十分
}

判定方法早見表

実務で迷った際にすぐ参照できる早見表です。

目的推奨される方法
nullのみを判定value === nullif (user === null)
undefinedのみを判定value === undefinedif (settings === undefined)
null/undefinedを一括判定value == nullif (data == null)
null/undefined/空文字を判定!valueif (!input)
デフォルト値設定(0/falseも有効)value ?? defaultValueport ?? 3000
デフォルト値設定(空文字を置換)`value
ネストしたプロパティアクセスobj?.prop?.nesteduser?.profile?.avatar
nullチェック後のデフォルト値obj?.prop ?? defaultValueuser?.name ?? "匿名"

TypeScriptのバージョン別機能対応表

使用している機能がTypeScriptのどのバージョンから利用可能かを確認しましょう。

機能TypeScriptバージョン備考
strictNullChecks2.0+必須設定
Optional Chaining (?.)3.7+推奨
Nullish Coalescing (??)3.7+推奨
== nullすべて安全に使える
! (Non-null assertion)2.0+極力避ける

チーム開発で統一すべきコーディング規約

プロジェクト全体で一貫性を保つため、以下の規約を定めることを推奨します。

// 1. strictNullChecksは必ず有効化
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,  // または "strictNullChecks": true
  }
}

// 2. 型定義での使い分けルール
interface Example {
  // オプショナルプロパティは ? を使う
  optional?: string;

  // 明示的に「値がない」場合は null
  nullable: string | null;

  // 両方あり得る場合は両方書く
  maybeValue: string | null | undefined;
}

// 3. 判定方法の統一
// null/undefinedの一括判定には == null を使う
if (value == null) { }

// 個別判定には === を使う
if (value === null) { }
if (value === undefined) { }

// 4. モダンな構文を優先
// Optional Chainingを積極的に使う
const avatar = user?.profile?.avatar ?? "/default.png";

// 5. Non-null assertionは原則禁止
// ESLintで警告またはエラーにする
// "@typescript-eslint/no-non-null-assertion": "error"

TypeScriptのnull/undefined判定は、単なる構文の違いではなく、型安全性を確保し、バグを未然に防ぐための重要な設計判断です。

// この記事で学んだ内容を活かした実践例
interface UserProfile {
  name: string;
  email: string;
  bio?: string;
  avatar: string | null;
  settings?: {
    theme: "light" | "dark";
    notifications: boolean;
  };
}

function displayUserProfile(profile: UserProfile | null | undefined) {
  // 1. null/undefinedチェック
  if (profile == null) {
    return <EmptyState />;
  }

  // 2. Optional Chainingでネストしたプロパティに安全にアクセス
  const theme = profile.settings?.theme ?? "light";
  const notifications = profile.settings?.notifications ?? true;

  // 3. null/undefinedの判定を使い分け
  const bio = profile.bio ?? "自己紹介が未設定です";
  const avatar = profile.avatar ?? "/default-avatar.png";

  return (
    <div>
      <img src={avatar} alt={profile.name} />
      <h1>{profile.name}</h1>
      <p>{profile.email}</p>
      <p>{bio}</p>
      <div>
        <span>テーマ: {theme}</span>
        <span>通知: {notifications ? "ON" : "OFF"}</span>
      </div>
    </div>
  );
}

重要ポイント

  • strictNullChecksを有効にして型安全性を確保する
  • == nullでnullとundefinedを一括判定する(唯一の==使用例)
  • ===で厳密に区別する必要がある場合に使用する
  • ?.でネストしたプロパティに安全にアクセスする
  • ??でデフォルト値を設定する(0やfalseも有効な値として扱う)
  • !(Non-null assertion)は極力避ける
  • 型や状況に応じて適切な判定方法を選択する

これらを意識してコードを書くことで、実行時エラーを大幅に減らし、可読性と保守性の高いTypeScriptコードを実現できます。あなたのプロジェクトで、今日からこれらのベストプラクティスを実践してみてください。

あなたのサイトのURL、そろそろスリムにしませんか?
タイトルとURLをコピーしました