Typescriptのexhaustiveness checkがtsconfigのnoUnusedLocalsオプションに引っかかる
要約
Typescriptのnever型を用いたexhaustive checkで型エラー検知用の変数を使わない時に'変数名' is declared but its value is never read.のエラーが出る場合、tsconfigのnoUnusedLocalsオプションが有効になっていないかを確認するべき。
Typescriptのexhaustiveness checkとは
Typescriptのunion型に種類を追加した時、never型を用いて既存のif文やswitch文で対応漏れが発生していないかをチェックするexhaustiveness checkというテクニックがあります。
// Typescriptの記事より引用
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
// 例えば、shape.kindに"triangle"が増えた場合、上のcase文を増やさないと
// ここに到達する可能性が出てくるため、never型と不整合が生じ型エラーが起きる
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
問題と解決
このテクニックでは型検知用の変数を定義してそれをreturnしているが、それはあくまで検知用なので使いたくない場合がある。
例えば上記では_exhaustiveCheckをランタイムで返すと挙動がおかしくなるのでundefinedを返したいなど。
自分があるプロジェクトでそれをやろうとした時、'_exhaustiveCheck' is declared but its value is never read.というエラーが出てしまい、最初はLinterのエラーかと思ってESLintのno-unused-varsを消すなどして時間を消費してしまった。
結果的に、tsconfigでnoUnusedLocalsオプションがオンになっていたためtscエラーを起こしていたというオチだった、、、
このtscのエラーは// @ts-ignoreアノテーションを付与することにより回避できるが、その場合exhaustiveness checkもignoreされて本末転倒な感じになってしまう。このテクニックを使いたいならTypescriptのnoUnusedLocalsオプションはオフにしてeslintのno-unused-varsに移行した上で// eslint-disable-next-lineするのがいいと思う。
Discussion
こちらの記事にあるように、網羅性チェック用に拡張されたエラークラスを作成し、それを利用することで、
noUnusedLocalsを無視することなく網羅性をチェックすることができるかと思います!