TypeScriptの条件分岐をスマートに記述するためのTips

はじめに

こんにちは、ARCH チームの立川です。 今回が初めてのテックブログになります。

先日、社内で「条件分岐をスマートに評価する」というテーマで、TypeScript(JavaScript)における条件分岐の書き方について発表する機会がありました。古いコードに触れる中で、見通しの悪い記述を多く見かけることがあったため、発表に至った経緯があります。

この記事では、その発表内容をベースにコードの可読性を高める条件分岐のテクニックをご紹介します。基礎的な内容ではありますが、少しでも役立つヒントがあれば幸いです!

三項演算子をよりスマートに使うためのヒント

三項演算子は非常に便利ですが、状況によってはもっとシンプルで読みやすい代替手段があります。ここでは、等価な三項演算子と比較しながら、それらの方法を紹介します。

null 合体演算子( ?? )を活用する

null 合体演算子は、左辺が null または undefined の場合にのみ右の値を返します。それ以外の場合は左の値を返します。

⛔️ Bad

// job: string | null
job === null ? "無職" : job;

✅ Good

// job: string | null
job ?? "無職";

job ?? "無職"; と null 合体演算子を使うことで、 jobnull でも undefined でも「無職」というデフォルト値が適用されます。三項演算子を使用しても実現可能ですが、冗長なコードとなり見通しが悪くなります。

短絡評価( ||&& )を使いこなす

短絡評価を利用することで、三項演算子を使わずにコードを簡潔に記述できるケースが多くあります。まずは ||(OR 演算子) から見ていきましょう。

⛔️ Bad

// inputJob: string
inputJob === "" ? null : inputJob;

✅ Good

inputJob || null;

|| は左辺が Falsy な値の場合に右辺を評価します。null 合体演算子( ?? )と比較して右辺が評価される範囲が広いので、用途に応じて使い分けることが重要です。 Bad 例に比べて Good 例では inputJob をわざわざ二度書く必要はなく、読み手にとっても意図が明確に伝わりやすくなります。

次に &&(AND 演算子) についてです。

⛔️ Bad

// userName: string
userName ? `${userName} 様` : "";

✅ Good

userName && `${userName} 様`;

&& は左辺が Truthy な値の場合にのみ右辺を評価します。これにより、特定の条件が真の時にのみ何らかの処理を行いたい場合などに有効です。

オプショナルチェーン( ?. )で安全なプロパティアクセス

オプショナルチェーンは、オブジェクト(ネストしたオブジェクトも含む)のプロパティにアクセスする際に、 nullundefined が含まれていてもエラーにならず、 undefined を返す安全なアクセス方法です。

例えば、以下の User 型を考えてみましょう。

type User = {
  lastName: string;
  firstName: string;
  birthday?: {
    year: number;
    month: number;
    day: number;
  };
} | null;

この user オブジェクトの lastNameyear にアクセスすることを考えてみます。

⛔️ Bad

const lastName = user ? user.lastName : "";
const birthYear = user
  ? user.birthday
    ? user.birthday.year
    : undefined
  : undefined;

✅ Good

const lastName = user?.lastName ?? ""; // undefined の場合は null 合体演算子で空文字に
const birthYear = user?.birthday?.year;

先述の通り、プロパティにアクセスできない場合、オプショナルチェーンは undefined を返します。そのため、lastName の例のように undefined の場合のデフォルト値を指定するために null 合体演算子を併せて利用するケースもよくあります。

Bad 例のようにネストされたオブジェクトにアクセスする場合、各レベルで nullundefined のチェックを if 文や三項演算子で行うと、コードは非常に読みにくく複雑になり意図しないエラーが発生しやすくなります。

一方で Good 例ではオプショナルチェーンの性質により実行時エラーによる予期せぬ動作を防ぐことが可能です。

配列を活用した条件評価

複数の条件を評価する際、条件を配列に格納してから評価することで、コードの可読性が格段に向上することがあります。

every メソッドで AND 条件をスマートに

すべての条件が AND ( && ) で接続されている場合は、すべての条件を配列にまとめて every メソッドを利用することで、コードの見通しが良くなります。

⛔️ Bad

if (
  city === "東京都" &&
  hobby === "ドライブ" &&
  age >= 30 &&
  holdingCarPeriod >= 10
) {
  console.log(
    "東京都に住んでいて、ドライブが好きで、30歳以上で、10年以上車を所有している"
  );
}

✅ Good

const targetConditions = [
  city === "東京都",
  hobby === "ドライブ",
  age >= 30,
  holdingCarPeriod >= 10,
];
if (targetConditions.every((condition) => condition)) {
  console.log(
    "東京都に住んでいて、ドライブが好きで、30歳以上で、10年以上車を所有している"
  );
}

Bad 例のように複数の条件を && でつなげた場合、条件の数が多くなると、どこまでが一つの条件式なのか、全体として何をチェックしているのかが非常に分かりにくくなります。特に、複雑な条件が混ざると、括弧の付け間違いや論理エラーが発生しやすくなり、テストで発見しにくい潜在的なバグにつながることが少なくありません。新しい条件を追加する際にも、既存の長い行を修正することになり、ミスが発生しやすい傾向があります。

Good 例のように条件を配列に格納し、every メソッドを使うことで、各条件が独立した要素として明確に定義されます。新しい条件を追加する際も、配列に要素を追加するだけで済み、既存のコードへの影響を最小限に抑えられます。

some メソッドで OR 条件を簡潔に

すべての条件が OR ( || ) で接続されている場合は、すべての条件を配列にまとめて some メソッドを利用することで、コードの見通しが良くなります。

⛔️ Bad

if (
  jobCategory === "自動車業界" ||
  hobby === "ドライブ" ||
  holdingCarPeriod > 5
) {
  console.log("車好きかも");
}

✅ Good

const targetConditions = [
  jobCategory === "自動車業界",
  hobby === "ドライブ",
  holdingCarPeriod > 5,
];
if (targetConditions.some((condition) => condition)) {
  console.log("車好きかも");
}

Bad 例の || で連結された条件は、&& の場合と同様に条件の数が多くなると、コードが横に長く伸びてしまい、読みづらさや間違いやすさが顕著になります。Good 例では個々の条件が独立して管理されるため、追加や削除、変更が容易になり、条件式のミスや抜け漏れを防ぎやすくなります。

includes メソッドで複数の等価条件を簡潔に

複数の等価条件を記述する場合、includes メソッドを利用するとスマートに記述できます。

⛔️ Bad

if (job === "公務員" || job === "自営業" || job === "会社員") {
  console.log(job);
}

✅ Good

if (["公務員", "自営業", "会社員"].includes(job)) {
  console.log(job);
}

Bad 例のように、job === "公務員" || job === "自営業" || job === "会社員" といった記述は、同じ変数に対する複数の等価チェックを繰り返しています。このような記述は、条件の追加や変更のたびに冗長なコードを書き足す必要があり、タイポや条件の抜け漏れが発生しやすくなります。

Good 例では includes メソッドを使うことで、許容される値を配列として一箇所にまとめ、その中に job が含まれているかをシンプルにチェックできます。条件を追加したい場合も配列に要素を追加するだけで済むため、修正範囲が限定されバグの混入リスクを減らせます。

find メソッドで条件に合致する要素を抽出

複雑な switch 文や複数の if-else if を使う代わりに、find メソッドを利用して意図が明確で分かりやすい記述にすることができます。

⛔️ Bad

const year: number = 2015;
switch (year) {
  case 1996:
    console.log("Mission: Impossible");
    break;
  case 2000:
    console.log("Mission: Impossible 2");
    break;
  case 2006:
    console.log("Mission: Impossible III");
    break;
  case 2011:
    console.log("Mission: Impossible - Ghost Protocol");
    break;
  case 2015:
    console.log("Mission: Impossible - Rogue Nation"); // 該当
    break;
  case 2018:
    console.log("Mission: Impossible - Fallout");
    break;
  case 2023:
    console.log("Mission: Impossible - Dead Reckoning Part One");
    break;
  case 2025:
    console.log("Mission: Impossible - The Final Reckoning");
    break;
  default:
    console.log(undefined);
}

✅ Good

const movies: { name: string; year: number }[] = [
  { name: "Mission: Impossible", year: 1996 },
  { name: "Mission: Impossible 2", year: 2000 },
  { name: "Mission: Impossible III", year: 2006 },
  { name: "Mission: Impossible - Ghost Protocol", year: 2011 },
  { name: "Mission: Impossible - Rogue Nation", year: 2015 },
  { name: "Mission: Impossible - Fallout", year: 2018 },
  { name: "Mission: Impossible - Dead Reckoning Part One", year: 2023 },
  { name: "Mission: Impossible - The Final Reckoning", year: 2025 },
];
console.log(movies.find((movie) => movie.year === 2015)?.name); // "Mission: Impossible - Rogue Nation"

また、Map オブジェクトを使用することによっても同様のことを行えます。key のみを評価する場合はこちらの方法で良いと思います。

const movies = new Map<number, string>([
  [1996, "Mission: Impossible"],
  [2000, "Mission: Impossible 2"],
  [2006, "Mission: Impossible III"],
  [2011, "Mission: Impossible - Ghost Protocol"],
  [2015, "Mission: Impossible - Rogue Nation"],
  [2018, "Mission: Impossible - Fallout"],
  [2023, "Mission: Impossible - Dead Reckoning Part One"],
  [2025, "Mission: Impossible - The Final Reckoning"],
]);

// Map.get() で値を取得。存在しない場合は undefined を返す
console.log(movies.get(2015) ?? "該当なし"); // "Mission: Impossible - Rogue Nation"
console.log(movies.get(1990) ?? "該当なし"); // "該当なし"

Bad 例の switch 文は、条件が増えるごとに case 文が縦に長く伸び、各 case の最後に break を書き忘れると、意図しない処理が実行される「フォールスルー」というバグにつながります。また、データとロジックが混在しており、新しいデータを追加するたびに switch 文全体を修正する必要があり、変更のたびにバグを混入させるリスクが高まります。

一方、 Good 例ではデータを配列(または Map オブジェクト)として定義し、find メソッドを使って条件に合致する要素を検索しています。このようにデータとロジックを分離することで、コードがはるかに見通しやすくなります。データが増えたとしても、配列に要素を追加するだけで済み、既存のロジックを変更する必要がありません。

まとめ

この記事では、TypeScript/JavaScript における条件分岐をよりスマートに記述するためのいくつかのテクニックをご紹介しました。

  • 三項演算子の代替 : ?? (null 合体演算子)、 || (OR 短絡評価)、&& (AND 短絡評価)、 ?. (オプショナルチェーン) を適切に使い分けることで、より簡潔で意図が明確なコードになります。
  • 配列メソッドの活用 : everysomeincludesfind を使うことで、複数の条件を扱う際の if 文や switch 文が格段に読みやすくなります。ロジックとデータを分離しコードの冗長性を排除することで、変更によるバグのリスクを大幅に減らして保守性を高めることができます。

Bad 例と比較して Good 例のコードは単に短いだけでなく、コードの意図がより明確になり、予期せぬ挙動やバグの発生リスクを低減するというメリットがあります。

最後までお読みいただきありがとうございました!今回の内容が、皆さんの開発の一助となれば嬉しいです。