📘

TypeScript コーディングテクニック #1 【条件分岐編】

2024/02/24に公開

ごあいさつ

はじめまして、 somnicattus と申します。普段は TypeScript を使って Vue のフロントエンドや Web API を開発しています。

シリーズとして、普段使っている TypeScript (JavaScript) のコーディングテクニックや意識していること紹介していきます。

想定する読者は、TypeScript をある程度使ったうえでさらにコード品質を高めたい方です。初心者向けではないと思います。

第 1 弾は、条件分岐する処理の書き方のテクニックです。

if 文と switch 文の弱点

ifswitch は基本的な条件分岐の文ですが、少々力不足な点があります。

  1. 独立した文なので式の途中に埋め込めない
  2. (switch) break の有無で挙動が変わる (fall-through) 見通しの悪さ
  3. 条件が変更・追加されたときに実装漏れが起こりやすい
  4. ネストが増えてコードが縦にも横にも長くなる

if 文の代替(特定の条件で特定の値を使う)

条件によって値を変えたりプロパティを追加したりする処理をするとき、 if を使ってしまうとうまく定数や読み取り専用にすることができません。

プログラミングで使うデータは原則として 「定数」や「読み取り専用」であるべきです。
手元を離れた変更可能な値はいつどこで変更されるか予想できないので、処理の見通しを著しく悪化させます。

if を使った文 (statement) の代わりに、 null 合体演算子 (??) や三項演算子 (?:) を使った式 (expression) を使うようにすると、条件によって変わる値を定数にしたり、条件によって内容が変わるオブジェクトを読み取り専用にしたりできます。

言葉で説明するのは難しいですが、以下に紹介するソースコードを読めばわかると思います。

値の宣言

定数の値を特定の条件で変えることができます。

declare-variable-constant.ts
// 定数 (const) で宣言することで値が変更されない
const value = givenValue ?? DEFAULT_VALUE;

// if 文で書く場合、変数 (let) で宣言する必要がある
let value = DEFAULT_VALUE;
if (givenValue != null) value = givenValue;

オブジェクトの宣言

スプレッド構文 (...) と組み合わせることで、読み取り専用のオブジェクトに特定の条件でプロパティを増やすことができます。

daclare-variable-readonly-object.ts
const object = {
  name: 'sample',
  ...(givenValue == null ? {} : { value: givenValue }),
// const assertion によって読み取り専用に
} as const;
// => { name: 'sample' }
// または
// => { name: 'sample', value: 'givenValue' }

// if 文で書く場合、読み取り専用にできない
// 型注釈も必要になる
const object: { name: 'sample'; value?: string } = { name: 'sample' };
if (givenValue != null) object.value = givenValue;

値が undefined のプロパティが増えることを許容できるなら以下の書き方でよいです。

object-with-key-of-undefined.ts
const object = { name: 'sample', value: givenValue } as const;
// => { name: 'sample', value: undefined }

https://typescriptbook.jp/reference/values-types-variables/const-assertion

その他

同様に、特定条件下で読み取り専用オブジェクトの値を変える、特定条件下で読み取り専用の配列に要素を追加するなども可能です。

注意点として、これらの用途で or 演算子 (||) を使うと Falsy な値の扱いについて混乱の元になります|| が使える場面でも ?: を使うようにすると、ミスや勘違いが起こりにくいです。

avoid-using-or-operator-as-declaring-variables.ts
// Falsy な値の扱いが明確でないのでミスかもしれない
const value = givenValue || DEFAULT_VALUE;

// Falsy な値の扱いを明確に書くと意図が伝わる
const value = givenValue == null || givenValue === '' ? DEFAULT_VALUE : givenValue;

if 文の使いどころ

定数宣言の問題を抜きにすれば、if 文で書かれた条件分岐はとても読みやすいです。 if 文を無理に and 演算子 (&&) で書き替えることはお勧めしません

avoid-using-and-operator-as-branching.ts
// ただの論理演算に見える
 condition === 'complete' && doSomething();

 // if 文で書いた方が意図が明確になる
if (condition === 'complete') doSomething();

そのほか if 文を効果的に使うテクニックとして 早期リターン (スロー) があります。条件分岐のネストを深くさせないためのテクニックで、コードの見やすさが劇的に改善します。

https://zenn.dev/media_engine/articles/early_return

switch 文の代替

switch の fall-through の挙動は非常に見通しが悪いです。 ESLint などのリンターを使って fall-through を禁止することが多いと思いますがそれよりも良い方法があります。

条件を処理に対応させるするオブジェクトを fall-through のない switch として使用し、分岐条件が追加されたときに実装漏れに気付けるように satisfies operator を使います。

switch-by-mapping-object.ts
// 条件のリスト
type Option = 0 | 1;

// 条件を処理に対応させるオブジェクト
const doSomethingMap = {
  0: doSomething0,
  1: doSomething1,
  // Option に 2 が追加されるとプロパティ不足のエラーが出る
} as const satisfies Record<Option, () => void>
doSomethingMap[givenOption]();

// switch 文で書く場合、 fall-through を意識して読み書きする必要がある
switch (givenOption) {
  case 1:
    doSomething1();
    break;
  case 0:
    doSomething0();
    break;
  // Option 型に 2 が追加されても実装漏れに気づけない
  default:
    throw new Error(`Invalid option: '${givenOption}'.`);
}

https://zenn.dev/monicle/articles/62974d8e9f704d

switch 文の使いどころ

switch の用途は、 if と比べて限られています。 switch はマッピングオブジェクトで完全に代替できます(忌まわしい fall-through を使わないなら)。基本的に可読性が高く変更に強いマッピングオブジェクトを使ったほうが良いと思います。

次回は

配列のループ処理についてのテクニックを紹介しようと思います。

Discussion