TypeScript Nullish Coalescing演算子の実務活用ガイド|業務で役立つ実装パターン

未分類

TypeScript Nullish Coalescing演算子の実務活用ガイド|業務で役立つ実装パターン

はじめに:nullish coalescing演算子とは

TypeScriptで開発していると、値が存在しない場合のハンドリングは避けて通れない課題です。特にAPIからのレスポンスやユーザー入力を扱う場合、null や undefined の判定が必要になります。

「nullish coalescing演算子」(??)は、ES2020で導入された比較的新しい機能で、null や undefined に対してのみデフォルト値を適用する演算子です。従来の || (OR演算子)と異なり、0やempty stringなどのfalsy値を区別する点が特徴です。

簡易的な解説:Nullish Coalescing演算子の基本

??(Nullish Coalescing)と ||(OR演算子)の違い

実務で多くの開発者が最初につまずくのが、?? と || の使い分けです。以下のコードを見てみましょう。

// OR演算子(||)の場合
const value1 = 0 || 10;  // 10が返される(0はfalsyと判定される)
const value2 = '' || 'default';  // 'default'が返される
const value3 = false || true;  // trueが返される

// Nullish Coalescing演算子(??)の場合
const value4 = 0 ?? 10;  // 0が返される(0はnullish値ではない)
const value5 = '' ?? 'default';  // ''が返される(空文字はnullish値ではない)
const value6 = false ?? true;  // falseが返される
const value7 = null ?? 'default';  // 'default'が返される
const value8 = undefined ?? 'default';  // 'default'が返される

重要な点は、nullish coalescing演算子が「null と undefined のみ」を特別視し、その他のfalsy値(0, false, empty string)は区別することです。

型安全性との関係

TypeScriptの strictNullChecks モードを有効にしている場合、nullish coalescing演算子は型推論をより正確に行います。

interface User {
  name: string;
  age?: number;  // 型は number | undefined
  email?: string;  // 型は string | undefined
}

const user: User = { name: 'Tanaka' };

// ??を使うことで、型推論が正確
const age: number = user.age ?? 20;  // 型は number
const email: string = user.email ?? 'no-email@example.com';  // 型は string

実務での業務ユースケース

ユースケース1:API レスポンスのデータ処理

最も一般的な使用例は、外部APIからのレスポンスハンドリングです。APIは予期しないデータ構造を返すことがあり、その際のフォールバック処理が必要です。

// APIレスポンスの型定義
interface ProductResponse {
  id: number;
  name: string;
  price?: number;
  stock?: number;
  discount?: number;
  description?: string;
}

interface ProcessedProduct {
  id: number;
  displayName: string;
  finalPrice: number;
  availableStock: number;
  summary: string;
}

// 実務コード:複数のAPIレスポンスを統合する場合
async function fetchAndProcessProduct(productId: number): Promise {
  const response = await fetch(`/api/products/${productId}`);
  const data: ProductResponse = await response.json();

  // nullish coalescing を使ったデフォルト値の設定
  const finalPrice = (data.price ?? 0) * (1 - (data.discount ?? 0) / 100);
  
  return {
    id: data.id,
    displayName: data.name ?? '商品名未設定',
    finalPrice: Math.round(finalPrice),
    availableStock: data.stock ?? 0,
    summary: data.description ?? '説明はありません'
  };
}

ユースケース2:フォーム入力値の正規化

ユーザーフォームの送信処理では、入力が空の場合、null、undefinedの3パターンが起こり得ます。nullish coalescing で統一的に処理できます。

interface RegistrationForm {
  firstName: string | null;
  lastName: string | null;
  phoneNumber?: string;
  companyName: string | undefined;
  yearsOfExperience?: number;
}

function normalizeFormData(form: RegistrationForm): object {
  return {
    firstName: form.firstName ?? '',
    lastName: form.lastName ?? '',
    fullName: `${form.firstName ?? ''}${form.lastName ?? ''}`.trim() || '未設定',
    phone: form.phoneNumber ?? 'N/A',
    company: form.companyName ?? '個人',
    experience: form.yearsOfExperience ?? 0,
    isCompanyEmployee: (form.companyName ?? null) !== null
  };
}

// 使用例
const formData: RegistrationForm = {
  firstName: null,
  lastName: '田中',
  phoneNumber: undefined,
  companyName: undefined,
  yearsOfExperience: 0  // 0年の経験は有効なデータ
};

const normalized = normalizeFormData(formData);
console.log(normalized);
// 出力:
// {
//   firstName: '',
//   lastName: '田中',
//   fullName: '田中',
//   phone: 'N/A',
//   company: '個人',
//   experience: 0,
//   isCompanyEmployee: false
// }

ユースケース3:設定値の多層的なフォールバック

実務では、ユーザー設定 → デフォルト設定 → システム設定といった多階層のフォールバックが必要になることがあります。

interface UserPreference {
  theme?: string;
  language?: string;
  fontSize?: number;
}

interface AppConfig {
  defaultTheme: string;
  defaultLanguage: string;
  defaultFontSize: number;
}

const appConfig: AppConfig = {
  defaultTheme: 'light',
  defaultLanguage: 'ja',
  defaultFontSize: 14
};

function getUserSettings(userPref: UserPreference): AppConfig {
  return {
    defaultTheme: userPref.theme ?? appConfig.defaultTheme,
    defaultLanguage: userPref.language ?? appConfig.defaultLanguage,
    defaultFontSize: userPref.fontSize ?? appConfig.defaultFontSize
  };
}

// 使用例
const user1Pref: UserPreference = { theme: 'dark' };
const user1Settings = getUserSettings(user1Pref);
// { defaultTheme: 'dark', defaultLanguage: 'ja', defaultFontSize: 14 }

const user2Pref: UserPreference = {};
const user2Settings = getUserSettings(user2Pref);
// { defaultTheme: 'light', defaultLanguage: 'ja', defaultFontSize: 14 }

実装コード:実践的なビジネスロジック

例1:ECサイトの商品データ整形

複数のデータソース(在庫システム、推奨エンジン、レビューシステム)から取得したデータを統合する実務的なコード例です。

interface InventoryData {
  productId: number;
  stock?: number;
  warehouseLocation?: string;
}

interface RecommendationData {
  productId: number;
  score?: number;
  reason?: string;
}

interface ReviewData {
  productId: number;
  averageRating?: number;
  reviewCount?: number;
}

interface MergedProductData {
  productId: number;
  availableStock: number;
  location: string;
  recommendationScore: number;
  reason: string;
  rating: number;
  reviewCount: number;
}

function mergeProductData(
  inventory: InventoryData,
  recommendation: RecommendationData,
  review: ReviewData
): MergedProductData {
  return {
    productId: inventory.productId,
    availableStock: inventory.stock ?? 0,
    location: inventory.warehouseLocation ?? '未指定',
    recommendationScore: recommendation.score ?? 0,
    reason: recommendation.reason ?? 'アルゴリズムなし',
    rating: review.averageRating ?? 0,
    reviewCount: review.reviewCount ?? 0
  };
}

// 実際の使用例
const inventory: InventoryData = { productId: 1, stock: 5 };
const recommendation: RecommendationData = { productId: 1, score: 0.85 };
const review: ReviewData = { productId: 1, averageRating: 4.5, reviewCount: 120 };

const merged = mergeProductData(inventory, recommendation, review);
console.log(merged);

例2:ユーザー認証トークン管理

API呼び出し時に、複数の場所からトークンを取得し、優先度順にフォールバックする処理です。

interface AuthContext {
  headers?: Record;
  localStorage?: Storage;
  sessionStorage?: Storage;
  environment?: NodeJS.ProcessEnv;
}

class TokenManager {
  private context: AuthContext;

  constructor(context: AuthContext) {
    this.context = context;
  }

  getAuthToken(): string {
    // 優先度:リクエストヘッダー → ローカルストレージ → セッションストレージ → 環境変数 → デフォルト
    const headerToken = this.context.headers?.['Authorization']?.replace('Bearer ', '');
    const localToken = this.context.localStorage?.getItem('auth_token');
    const sessionToken = this.context.sessionStorage?.getItem('auth_token');
    const envToken = this.context.environment?.['AUTH_TOKEN'];

    return (
      headerToken ??
      localToken ??
      sessionToken ??
      envToken ??
      'ANONYMOUS_TOKEN'
    );
  }

  getRefreshToken(): string | null {
    const localRefresh = this.context.localStorage?.getItem('refresh_token');
    const sessionRefresh = this.context.sessionStorage?.getItem('refresh_token');

    return localRefresh ?? sessionRefresh ?? null;
  }

  isAuthenticated(): boolean {
    const token = this.getAuthToken();
    return token !== 'ANONYMOUS_TOKEN' && token !== null;
  }
}

// 使用例
const context: AuthContext = {
  headers: { 'Authorization': 'Bearer token123' },
  localStorage: typeof window !== 'undefined' ? window.localStorage : undefined,
  sessionStorage: typeof window !== 'undefined' ? window.sessionStorage : undefined
};

const manager = new TokenManager(context);
const token = manager.getAuthToken();
console.log(manager.isAuthenticated());

よくある応用パターン

パターン1:チェーン処理での連続適用

nullish coalescing演算子は連続して使用できます。複数の条件付きプロパティをチェーンする場合に有効です。

interface Config {
  global?: {
    timeout?: number;
    retries?: number;
  };
}

interface RequestOptions {
  timeout?: number;
  retries?: number;
}

function buildRequestConfig(
  globalConfig?: Config,
  userOptions?: RequestOptions
): { timeout: number; retries: number } {
  return {
    // ユーザー設定 ?? グローバル設定 ?? デフォルト値
    timeout: userOptions?.timeout ?? globalConfig?.global?.timeout ?? 30000,
    retries: userOptions?.retries ?? globalConfig?.global?.retries ?? 3
  };
}

// テストケース
const config: Config = { global: { timeout: 5000, retries: 2 } };
const userOpts: RequestOptions = { timeout: 10000 };

const result = buildRequestConfig(config, userOpts);
console.log(result);  // { timeout: 10000, retries: 2 }

パターン2:条件分岐との組み合わせ

nullish coalescing で値を確定させた後、その値に基づいて処理を分岐させるパターンです。

interface Order {
  status?: 'pending' | 'processing' | 'shipped' | 'delivered';
  estimatedDays?: number;
  actualDays?: number;
}

function getDeliveryInfo(order: Order): string {
  const status = order.status ?? 'unknown';
  const days = order.actualDays ?? order.estimatedDays ?? 0;

  switch (status) {
    case 'pending':
      return `注文受け付け中。予定配送日数:${days}日`;
    case 'processing':
      return `処理中。予定配送日数:${days}日`;
    case 'shipped':
      return `発送済み。予定到着日数:${days}日`;
    case 'delivered':
      return `配送完了。実際の日数:${days}日`;
    default:
      return '不明なステータス';
  }
}

// 使用例
const order1: Order = { status: 'shipped', estimatedDays: 2 };
const order2: Order = { actualDays: 3 };  // statusがundefined
const order3: Order = {};  // すべてundefined

console.log(getDeliveryInfo(order1));  // "発送済み。予定到着日数:2日"
console.log(getDeliveryInfo(order2));  // "不明なステータス。予定配送日数:3日"
console.log(getDeliveryInfo(order3));  // "不明なステータス。予定配送日数:0日"

パターン3:オブジェクトのマージと値の確定

複数のオブジェクトをマージしながら、各プロパティの値を確定させるパターンです。

interface UserProfile {
  id: number;
  name?: string;
  avatar?: string;
  bio?: string;
}

interface DefaultProfile {
  name: string;
  avatar: string;
  bio: string;
}

const defaultProfile: DefaultProfile = {
  name: 'ユーザー',
  avatar: 'https://example.com/default-avatar.png',
  bio: 'プロフィールはまだ設定されていません'
};

function normalizeUserProfile(profile: UserProfile): Required> & { id: number } {
  return {
    id: profile.id,
    name: profile.name ?? defaultProfile.name,
    avatar: profile.avatar ?? defaultProfile.avatar,
    bio: profile.bio ?? defaultProfile.bio
  };
}

const user1: UserProfile = { id: 1, name: 'Alice', avatar: 'url-to-alice.jpg' };
const user2: UserProfile = { id: 2 };  // name, avatar, bioがundefined

const normalized1 = normalizeUserProfile(user1);
const normalized2 = normalizeUserProfile(user2);

console.log(normalized2);
// {
//   id: 2,
//   name: 'ユーザー',
//   avatar: 'https://example.com/default-avatar.png',
//   bio: 'プロフィールはまだ設定されていません'
// }

注意点と落とし穴

注意点1:0や空文字列は有効な値

nullish coalescing で0や空文字列がある場合、それは削除されません。これは意図的な設計ですが、実装時に誤解されやすいです。

// 間違いやすい例
interface Product {
  discount?: number;  // 0%割引は有効
  name?: string;  // 空文字列は有効?
}

const product1: Product = { discount: 0 };
const product2: Product = { name: '' };

// 0%割引を「割引なし」と解釈するべきか、「0で割引」と解釈するべきか
const discount = product1.discount ?? 10;  // 0が返される(意図通り)
const name = product2.name ?? 'Unnamed';  // ''が返される

// 空文字列を許さない場合は、明示的にチェックが必要
const safeName = (product2.name ?? '').trim() || 'Unnamed';

注意点2:オプショナルチェーンとの組み合わせ

nullish coalescing は オプショナルチェーン(?.)と一緒に使うことが多いですが、順序に注意が必要です。

interface User {
  profile?: {
    settings?: {
      language?: string;
    };
  };
}

const user: User = {};

// 正しい使い方
const language1 = user.profile?.settings?.language ?? 'en';
console.log(language1);  // 'en'

// 避けるべき:?を忘れると参照エラー
// const language2 = user.profile.settings.language ?? 'en';  // エラー!

// nullish coalescingで問題ないが、可読性が下がる
const language3 = (user.profile && user.profile.settings && user.profile.settings.language) ?? 'en';

注意点3:型推論が想定と異なる場合

TypeScriptの型推論が期待と異なる場合があります。特に、複雑な条件が関わる場合は明示的に型を指定しましょう。

function processValue(input: string | null | undefined): string {
  // 型推論により result は string 型となる
  const result = input ?? 'default';
  return result;  // 型エラーなし
}

interface Response {
  data?: { value: number } | null;
}

const response: Response = { data: null };

// nullish coalescingで問題ないが、型注釈があると明確
const value: number = response.data?.value ?? 0;

// 複雑な場合は明示的に型を指定
function getConfig(raw: unknown): Record {
  if (typeof raw === 'object' && raw !== null) {
    return (raw as Record);
  }
  return {};
}

注意点4:JavaScriptの互換性

nullish coalescing演算子はES2020の機能です。古いブラウザやNode.jsバージョンをサポートする場合は、Babelでのトランスパイル設定が必要です。

// TypeScript設定 (tsconfig.json)
{
  "compilerOptions": {
    "target": "ES2020",  // または "ES2015" 以下の場合はBabel必須
    "module": "ESNext",
    "lib": ["ES2020"]
  }
}

// Babel設定 (古いブラウザ対応)
{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "browsers": ["last 2 versions"]
      }
    }]
  ]
}

実務での推奨実装パターン

ここまでの知識を踏まえて、実務での推奨パターンをまとめます。

// 【推奨】ビジネスロジックレイヤーでの使用
interface ApiResponse {
  code: number;
  data?: T;
  message?: string;
}

interface ProductData {
  id: number;
  name: string;
  price: number;
  stock: number;
}

class ProductService {
  async getProduct(id: number): Promise {
    const response: ApiResponse = await this.fetchApi(`/products/${id}`);
    
    if (response.code !== 200) {
      throw new Error(response.message ?? 'Unknown error');
    }

    const product = response.data;
    if (!product) {
      throw new Error('Product not found');
    }

    // nullish coalescing で安全なデータ処理
    return {
      id: product.id,
      name: product.name ?? 'No name',
      price: product.price ?? 0,
      stock: product.stock ?? 0
    };
  }

  private async fetchApi(path: string): Promise> {
    const response = await fetch(path);
    return response.json();
  }
}

// 【推奨】UIレイヤーでの使用
function ProductCard({ product }: { product: ProductData }) {
  return (
    

{product.name}

価格: ¥{product.price.toLocaleString()}

在庫: {product.stock > 0 ? `${product.stock}個` : 'なし'}

); }

まとめ

TypeScriptの nullish coalescing 演算子(??)は、null と undefined に対して明確にデフォルト値を設定できる便利な機能です。

主なポイント:

  • ?? は「null と undefined のみ」に対して反応し、0や空文字列は有効な値として扱う
  • || と異なり、falsy値を区別するため、より正確なデータハンドリングが可能
  • APIレスポンス処理、フォーム入力、設定値のフォールバックなど、実務での使用場面は多い
  • オプショナルチェーン(?.)と組み合わせることで、深いオブジェクト参照も安全に行える
  • 型推論をサポートするため、TypeScriptのstrictNullChecksモードでの開発が進める
  • ES2020の機能のため、古い環境ではBabelでのトランスパイルが必要

実務では、「データが存在しない可能性がある」という局面が頻繁に現れます。nullish coalescing演算子を適切に活用すれば、コードの安全性と可読性の両立が実現でき、バグの減少にもつながります。特にAPIとの連携やユーザー入力処理で、この演算子の価値が最も発揮されることを意識して使い分けてください。

タイトルとURLをコピーしました