TypeScript optional chainingの実務的な使い方|nullチェックを効率化するパターン集
公開日:2024年1月
はじめに:Optional Chainingとは
Web開発をしていると、JavaScriptやTypeScriptで「このプロパティが存在するかわからない」という状況は日常茶飯事です。従来は複雑なnullチェックを書く必要がありましたが、Optional Chaining(?. 演算子)を使うことでコードをシンプルに、かつ安全に書くことができます。
Optional Chainingはプロパティが存在しない場合に自動的にundefinedを返す演算子です。これにより、深い階層のオブジェクトにアクセスする際のnull/undefined チェックを簡潔に書けます。
本記事では、実務で頻出するパターンを中心に、Optional Chainingの使い方と注意点を解説します。
簡易的な解説:Optional Chainingの基本
Optional Chainingには3つの形式があります。
1. プロパティアクセス(?.)
const user = {
name: \"田中太郎\",
profile: {
age: 30
}
};
// 従来の書き方
const age1 = user && user.profile && user.profile.age;
// Optional Chainingを使った書き方
const age2 = user?.profile?.age;
console.log(age2); // 30
2. メソッドの呼び出し(?.())
const obj = {
getName: () => \"太郎\",
getAge: null
};
// メソッドが存在するかチェック
const name = obj?.getName?.(); // \"太郎\"
const age = obj?.getAge?.(); // undefined(エラーにならない)
3. 配列のインデックスアクセス(?.[])
const items = [\"りんご\", \"みかん\"];
const first = items?.[0]; // \"りんご\"
const fourth = items?.[3]; // undefined
const nullItems = null;
const invalid = nullItems?.[0]; // undefined(エラーにならない)
これら3つの形式を理解することが、実務での活用の第一歩です。
業務でのユースケース
実際のプロジェクトでは、どのような場面でOptional Chainingが活躍するのでしょうか。代表的なケースを3つご紹介します。
ユースケース1:API レスポンス処理
バックエンドからのAPIレスポンスの形式が不確定な場合、Optional Chainingは非常に有効です。特に外部APIを使う場合、エラーハンドリング時にレスポンス構造が変わることがあります。
ユースケース2:フォーム入力値の検証
ユーザーが入力したフォームデータは、常に期待通りの構造を持つとは限りません。入力値の階層構造をチェックする際にOptional Chainingが役立ちます。
ユースケース3:条件付きレンダリング(React)
Reactコンポーネントでuserデータが存在するかどうかに応じて表示を変える場合、Optional Chainingで簡潔に条件判定できます。
実装コード:実務パターン別
パターン1:APIレスポンス処理
// APIレスポンスの型定義
interface ApiResponse {
data?: {
user?: {
id: number;
name: string;
settings?: {
theme?: string;
notifications?: boolean;
};
};
};
error?: {
code: string;
message: string;
};
}
// 実務コード:Axiosを使ったAPI呼び出し
import axios from 'axios';
async function fetchUserData(userId: number) {
try {
const response = await axios.get<ApiResponse>(`/api/users/${userId}`);
// Optional Chainingで安全にアクセス
const userName = response.data?.data?.user?.name ?? \"ゲスト\";
const theme = response.data?.data?.user?.settings?.theme ?? \"light\";
const isNotificationEnabled = response.data?.data?.user?.settings?.notifications ?? true;
return {
userName,
theme,
isNotificationEnabled
};
} catch (error) {
const errorCode = (error as any)?.response?.data?.error?.code ?? \"UNKNOWN_ERROR\";
console.error(`エラーが発生しました: ${errorCode}`);
throw error;
}
}
// 使用例
fetchUserData(1).then(user => {
console.log(`ユーザー名: ${user.userName}`);
});
パターン2:フォーム検証とデータ変換
// フォーム送信時のデータ構造
interface FormData {
personal?: {
firstName: string;
lastName: string;
email?: string;
};
address?: {
prefecture: string;
city?: string;
postalCode?: string;
};
agreement?: {
terms: boolean;
privacy?: boolean;
};
}
function validateAndNormalizeFormData(formData: FormData) {
// Optional Chainingで深い階層に安全にアクセス
const firstName = formData.personal?.firstName;
const lastName = formData.personal?.lastName;
const email = formData.personal?.email ?? \"\";
const prefecture = formData.address?.prefecture;
const city = formData.address?.city ?? \"未指定\";
const agreeToTerms = formData.agreement?.terms ?? false;
const agreeToPrivacy = formData.agreement?.privacy ?? false;
// バリデーション
const errors: string[] = [];
if (!firstName || firstName.trim() === \"\") {
errors.push(\"名前は必須です\");
}
if (!prefecture) {
errors.push(\"都道府県は必須です\");
}
if (!agreeToTerms) {
errors.push(\"利用規約への同意は必須です\");
}
if (errors.length > 0) {
return { success: false, errors };
}
return {
success: true,
data: {
fullName: `${firstName} ${lastName}`,
email,
address: `${prefecture}${city}`,
consents: {
terms: agreeToTerms,
privacy: agreeToPrivacy
}
}
};
}
// 使用例
const result = validateAndNormalizeFormData({
personal: {
firstName: \"田中\",
lastName: \"太郎\",
email: \"tanaka@example.com\"
},
address: {
prefecture: \"東京都\"
},
agreement: {
terms: true
}
});
if (result.success) {
console.log(result.data);
}
パターン3:React コンポーネントでの使用
import React from 'react';
interface User {
id: number;
name: string;
profile?: {
avatar?: string;
bio?: string;
socialLinks?: {
twitter?: string;
github?: string;
};
};
subscription?: {
plan: \"free\" | \"premium\";
expiresAt?: string;
};
}
interface UserProfileProps {
user: User | null;
isLoading: boolean;
}
export const UserProfile: React.FC<UserProfileProps> = ({ user, isLoading }) => {
// Optional Chainingで存在確認と値取得を同時に行う
const avatarUrl = user?.profile?.avatar ?? \"/default-avatar.png\";
const bio = user?.profile?.bio ?? \"プロフィールが未設定です\";
const twitterUrl = user?.profile?.socialLinks?.twitter;
const subscriptionPlan = user?.subscription?.plan ?? \"free\";
const expiryDate = user?.subscription?.expiresAt;
if (isLoading) {
return <div>読み込み中...</div>;
}
if (!user) {
return <div>ユーザー情報が見つかりません</div>;
}
return (
<div className=\"user-profile\">
<img src={avatarUrl} alt={user.name} />
<h2>{user.name}</h2>
<p>{bio}</p>
{twitterUrl && (
<a href={twitterUrl} target=\"_blank\" rel=\"noopener noreferrer\">
Twitter
</a>
)}
<div className=\"subscription-info\">
<span>プラン: {subscriptionPlan}</span>
{expiryDate && subscriptionPlan === \"premium\" && (
<span>有効期限: {new Date(expiryDate).toLocaleDateString('ja-JP')}</span>
)}
</div>
</div>
);
};
パターン4:Utility関数の作成
Optional Chainingを活用したヘルパー関数も実務では重要です。再利用可能なユーティリティ関数を作成することで、コード全体の保守性が向上します。
// Optional Chainingを活用したユーティリティ関数群
/**
* オブジェクトから安全に値を取得するヘルパー関数
* @param obj - 対象オブジェクト
* @param path - ドット記法のパス(例: \"user.profile.name\")
* @param defaultValue - デフォルト値
*/
function getNestedValue<T = any>(
obj: any,
path: string,
defaultValue: T
): T {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
current = current?.[key];
if (current === undefined) {
return defaultValue;
}
}
return current as T;
}
// 実際の使用例
interface ComplexData {
company?: {
department?: {
team?: {
lead?: {
name: string;
};
};
};
};
}
const data: ComplexData = {
company: {
department: {
team: {
lead: { name: \"山田太郎\" }
}
}
}
};
const leadName = getNestedValue(data, \"company.department.team.lead.name\", \"不明\");
console.log(leadName); // \"山田太郎\"
// ネストが浅い場合
const invalidPath = getNestedValue(data, \"company.invalid.path.value\", \"デフォルト\");
console.log(invalidPath); // \"デフォルト\"
よくある応用パターン
応用1:Nullish Coalescing(??)との組み合わせ
Optional Chainingと Nullish Coalescing(??)を組み合わせると、falsy値の処理をより細かく制御できます。
// 例:ユーザーの割引率設定
interface UserSettings {
discountRate?: number; // 0も有効な値
}
const settings: UserSettings = { discountRate: 0 };
// ❌ 間違い:0はfalsy値なのでデフォルトに置き換わる
const discount1 = settings.discountRate || 10; // 10になってしまう
// ✅ 正解:??を使うことで0を有効な値として扱う
const discount2 = settings?.discountRate ?? 10; // 0が返される
応用2:メソッドチェーンとの組み合わせ
interface Logger {
error?: (msg: string) => Logger;
warning?: (msg: string) => Logger;
info?: (msg: string) => Logger;
}
const logger: Logger | null = {
error: (msg) => {
console.error(msg);
return logger;
}
};
// Optional Chainingでメソッドチェーンを安全に実行
logger
?.error(\"エラーが発生しました\")?
.warning(\"警告です\")?
.info(\"処理完了\");
応用3:配列メソッドとの組み合わせ
interface ApiData {
results?: Array<{
id: number;
name: string;
tags?: string[];
}>;
}
function processApiResults(data: ApiData) {
// Optional Chainingと配列メソッドの組み合わせ
const allTags = data.results
?.flatMap(item => item.tags ?? [])
.filter((tag, index, array) => array.indexOf(tag) === index)
.sort();
return allTags ?? [];
}
// 使用例
const result = processApiResults({
results: [
{ id: 1, name: \"Item1\", tags: [\"js\", \"ts\"] },
{ id: 2, name: \"Item2\", tags: [\"ts\", \"react\"] }
]
});
console.log(result); // [\"js\", \"react\", \"ts\"]
注意点と落とし穴
注意1:Optional Chainingは実行を短絡させる
// チェーンのどこかでundefinedになると、後続の処理は実行されない
let callCount = 0;
const obj = {
getValue: () => {
callCount++;
return undefined;
}
};
// getValue()は実行されるが、後続の処理は実行されない
const result = obj?.getValue?.()?.toUpperCase?.();
console.log(callCount); // 1
console.log(result); // undefined
注意2:Optional Chainingは代入の左辺では使えない
const obj: any = {};
// ❌ エラー:Optional Chainingは代入の左辺では使用不可
// obj?.property = \"value\";
// ✅ 代わりに以下の方法を使う
if (obj) {
obj.property = \"value\";
}
注意3:Optional Chainingとして機能しないケース
const obj = {
value: 0,
empty: \"\",
isFalse: false
};
// Optional Chainingはnull/undefinedだけを判定する
// 0, \"\", false はそのまま返される
const val1 = obj?.value; // 0
const val2 = obj?.empty; // \"\"
const val3 = obj?.isFalse; // false
// falsy値をデフォルト値で置き換えたい場合は??を使う
const val1Default = obj?.value ?? 10; // 10ではなく0が返される
const val2Default = obj?.empty ?? \"N/A\"; // \"\"が返される
注意4:型安全性の確認
// TypeScriptの厳格モード(strict: true)では、
// Optional Chainingの結果は必ずundefined型を含む
interface User {
name: string;
email?: string;
}
const user: User = { name: \"太郎\" };
// emailの型は string | undefined
const email = user?.email;
// そのためメソッド呼び出しには注意
// ❌ エラー:emailがundefinedの可能性がある
// email.toUpperCase();
// ✅ 正しい書き方
email?.toUpperCase();
// または型ガード
if (email) {
email.toUpperCase();
}
実務Tips:ベストプラクティス
Tip1:デフォルト値の設定方針を統一する
プロジェクト全体でデフォルト値の設定方法を統一することで、保守性が向上します。
// 統一されたパターン
const DEFAULT_CONFIG = {
timeout: 5000,
retries: 3,
theme: \"light\" as const
};
interface Config {
timeout?: number;
retries?: number;
theme?: \"light\" | \"dark\";
}
function createConfig(overrides?: Config): Required<Config> {
return {
timeout: overrides?.timeout ?? DEFAULT_CONFIG.timeout,
retries: overrides?.retries ?? DEFAULT_CONFIG.retries,
theme: overrides?.theme ?? DEFAULT_CONFIG.theme
};
}
Tip2:複雑なネストはリファクタリングを検討
Optional Chainingの連鎖が長すぎる場合は、データ構造を見直すことを検討しましょう。
// 改善前:ネストが深すぎる
const value1 = data?.company?.department?.team?.lead?.profile?.contact?.email;
// 改善後:中間の型を活用
interface CompanyHierarchy {
getTeamLead(): Employee | null;
}
interface Employee {
getContactEmail(): string | null;
}
const value2 = data?.getTeamLead()?.getContactEmail();
Tip3:テストコードで edge cases をカバーする
import { describe, it, expect } from 'vitest';
describe('Optional Chaining with real data', () => {
it('should safely access nested undefined properties', () => {
const nullUser = null;
expect(nullUser?.profile?.name).toBeUndefined();
});
it('should handle undefined in the middle of chain', () => {
const user = { name: \"太郎\" }; // profileがない
expect(user?.profile?.age).toBeUndefined();
});
it('should return zero as valid value', () => {
const config = { retries: 0 };
const retries = config?.retries ?? 3;
expect(retries).toBe(0);
});
it('should work with array access', () => {
const items: string[] | null = null;
expect(items?.[0]).toBeUndefined();
});
});
まとめ
TypeScriptのOptional Chainingは、実務で頻出するnull/undefinedのチェックをシンプルに、かつ安全に記述するための強力なツールです。
重要なポイント:
- 基本形式を理解する:プロパティアクセス(?.)、メソッド呼び出し(?.())、配列アクセス(?.[])の3つを使い分ける
- Nullish Coalescingと組み合わせる:??演算子と併用することで、falsy値とnull/undefinedを区別できる
- APIレスポンスやフォーム処理で活躍:不確定な構造を持つデータの処理では必須のテクニック
- チェーンが長すぎないか確認:5階層以上のネストは、データ構造の見直しを検討する
- 型安全性を忘れずに:TypeScriptの型チェックを活用して、実行時エラーを未然に防ぐ
Optional Chainingを適切に使いこなすことで、コードの可読性と保守性が大幅に向上します。特にバックエンド連携の多いモダンなWebアプリケーション開発では、このテクニックなしには語れません。
本記事で紹介したパターンを参考に、実際のプロジェクトで活用していただければ幸いです。

