TypeScriptを使ってswitch文の網羅チェックをしてバグを防ぐ

以下のようなswitch文があるとします。

function makeCodes( code ) {
  switch( code ) {
    case 'AAA': return 'Code AAA'
    case 'BBB': return 'Code BBB'
  }
}

console.log(makeCodes('BBB'))

makeCodesでは必ず各コードごとにcase節を書く必要があるとします。

今の状態では、codeの種類はAAAとBBBの二つで、それぞれcase節が書かれていて問題ありません。

しかし、今後 CCC というコードが増えた時に、makeCode 内に CCC に対するcase 節を書き忘れる可能性があります。

もし、CCC に対する case 節を書き忘れた場合は undefined となってしまい想定した動きにならず、バグになります。

function makeCodes( code ) {
  switch( code ) {
    case 'AAA': return 'Code AAA'
    case 'BBB': return 'Code BBB'
  }
}

console.log(makeCodes('CCC')) //undefinedとなってしまう

このように、case 節の書き忘れを防ぐためにはどのような工夫をすれば良いでしょうか。

TypeScriptを使用することでこの問題を解消できます。

以下はTypeScriptを使ったコードです。

type Code = 'AAA' | 'BBB'

function exhaustiveCheck( param: never ) { }

function makeCodes( code: Code ) {
  switch( code ) {
    case 'AAA': return 'Code AAA'
    case 'BBB': return 'Code BBB'
    default: return exhaustiveCheck( code )
  }
}

まず、code の種類を型で定義します。

type Code = 'AAA' | 'BBB'

そして、exhaustiveCheck という関数を定義し、それを switch 文の default 節で実行します。

exhaustiveCheck の引数paramは never 型です。

そのため、もし makeCodes 関数の default節で code に never 型以外がきた場合は、その時点でコンパイルエラーになります。

それでは、仕様変更があり、code に CCC が追加された場合をみてます。

CCC が追加されたので、型に CCC を追加します。

しかし、CCC に対する case 節を書き忘れたとします。

type Code = 'AAA' | 'BBB' | 'CCC' // ここは変更した

function exhaustiveCheck( param: never ) { }

function makeCodes( code: Code ) {
  switch( code ) {
    case 'AAA': return 'Code AAA'
    case 'BBB': return 'Code BBB'
  // CCC に対する case 節は忘れた
    default: return exhaustiveCheck( code )
  }
}

その場合、以下のコンパイルエラーが発生します。

Argument of type 'string' is not assignable to parameter of type 'never'.

TypeScriptの型推論により、default節を通るのは code が CCC の時のみになりますが、CCC はstring 型のため、exhaustiveCheck の 引数の型の never と一致せずにコンパイルエラーになります

このように、case 節の追加忘れを事前に知ることができます

CCC に対する case 節を書いてあげることで、コンパイルエラーが解消します。

type Code = 'AAA' | 'BBB' | 'CCC'

function exhaustiveCheck( param: never ) { }

function makeCodes( code: Code ) {
  switch( code ) {
    case 'AAA': return 'Code AAA'
    case 'BBB': return 'Code BBB'
    case 'CCC': return 'Code CCC' // 追加する
    default: return exhaustiveCheck( code )
  }
}

console.log(makeCodes('CCC'));

CCC に対する case 文を追加すれば、default 節の exhaustiveCheck の引数は never 型となる(起こり得ない)ので、コンパイルエラーにはなりません。