プロジェクトを長い期間メンテナンスしていると、あれ?これなんでこんな書き方になってるんだっけ?という実装に時々出会います。他の人が書いたソースならともかく、自分で書いたにも関わらず、あれ?あれれ?と思うことが良くあります。
ロジックに対するあれれ?ならば、コメントを読んだり、設計ドキュメントを読んだり、実装の前後を読んだりすることで解消することもあるのですが、細かな実装方法に関してはなんだかさっぱりわからなくて、まぁいいか、このままにしちゃえ、ということもままあります。
最近特に気になっているのが、TypeScript で本来必要でないところに現れる謎の optional chain。動作には問題ないんですが、歯に何かが挟まっているように、一度気になりはじめると気になって仕方ありません。なんとかして削除したい、そんな記事です。
不要な optional chain とは
optional chain は nullish(null もしくは undefined)の可能性がある参照に対して、事前にその参照の値を検査することなしにそのプロパティにアクセスできるものですが、参照の型が nullish の可能性がなくても optional chain を使用することができます。(そもそも optional chain は JavaScript(ECMAScript)の機能であるため、型なんか気にするわけがありません。)
// TypeScript:例1 function foo(s: string) { const n = s?.length; // `?.` ではなく、`.` で十分だけれど ... }
例1では、型注釈によれば s
は nullish の可能性はありませんが、そんな s
に対しても optional chain ?.
が使えます。
JavaScript にトランスパイルした結果を見ると、まぁそうだよね、使えるよね、というのがよく分かると思います。 optional chain が使えない JavaScript にトランスパイル( undefined ではなく、おまけ:JavaScript にトランスパイルすると...
// JavaScript:例1のトランスパイル結果
function foo(s) {
const n = s?.length;
...
}
"target": "ES2019"
)すると、事前に検査する実装に変換されます。// JavaScript:例1のトランスパイル結果 ES2019 の場合
function foo(s) {
const n = s === null || s === void 0 ? void 0 : s.length;
...
}
void 0
(void 演算子)なんですね。
例1のような単純なケースだと「この ?.
は .
に置き換えても構わない」とすぐに判断できますが、例2のような複雑な型の場合はすぐには判断できません。この ?.
って必要なんだっけ?と迷いが生じます。 必要な箇所だけに ?.
が適用されている、そんな実装が理想的です。
// TypeScript:例2 とても複雑な型 function foo(s: SuperComplexType) { const n = s.abracadabra.presto?.chango.hocus?.pocus; ... }
この記事では「nullish の可能性がないものに対して適用されている optional chain ?.
」を「不要な optional chain」と呼びます。「不要な optional chain」を単純なプロパティアクセスなどに変換したい、というのがゴールです。
// TypeScript:例3 これがゴール function foo(s: string) { const n = s.length; // ← const n = s?.length; ... }
型注釈が nullish の可能性がある場合、 型注釈が nullish の可能性がない場合、 そこまでわかってるんなら警告ぐらい出してくれてもええやん TypeScript さん、と正直思います。
おまけ:
n
の型は...n
の型は number | undefined
になりますが、// TypeScript
function foo(s: string | undefined) {
const n = s?.length; // `n` の型は `number | undefined`
...
}
n
の型は number
になります。// TypeScript
function foo(s: string) {
const n = s?.length; // `n` の型は `number`
...
}
なんで不要な optional chain があるんだっけ?
次に、そもそもなぜ不要な optional chain が出現するのか考えます。
① もともとは必要だったんだよパターン
どこかで定義した型が、開発途中やバージョンアップの際に変わってしまった場合です。
当初は nullish の可能性があったために ?.
を使っていましたが、途中で nullish の可能性がなくなりました。トランスパイル時にも実行時にもエラーにならないので、変更に気づかずに不要な optional chain となってしまいました。
VS Code では型に応じて ?.
が自動補完されるため、そもそも nullish の可能性があるかどうかを意識しないまま初期実装してしまうこともあるでしょう。
// TypeScript:例4 // 最初は string | null だったんだけど type SomeType = string | null; function foo(s: SomeType) { const n = s?.length; // このときは `?.` が必要だった ... } // 途中で string になった type SomeType = string; function foo(s: SomeType) { // 利用側は変わらない const n = s?.length; // そして `?.` が残った ... }
?.
が残っていても動作に影響はありませんが、どこかのタイミングで修正してしまいたい実装です。
② 型定義と動作が一致していないんだよパターン
どこかで定義した型と実際の動作が一致していない場合です。
型定義では nullish の可能性がないとなっていますが、実際に動作させると null となる場合があり、実行時エラーを防ぐために仕方なく ?.
としています。
// TypeScript:例5 // 本当は null になる可能性があるのに、型ではそうなっていない type SomeType = string; function foo(s: SomeType) { const n = s?.length; // 実行時エラーになる場合があるので仕方なく `?.` とする ... }
この場合は ?.
を使う必要がありますが、コメントなどで記録を残しておかないと「なんでここ ?.
使ってるんだっけ?要らないよね、外しちゃえ。実行時エラーになるのか!」となりがちです。
不要な optional chain を撲滅するには
@typescript-eslint/no-unnecessary-condition を使うことで、不要な optional chain を単純なプロパティアクセスなどに自動的に変換することができます。
① もともとは必要だったんだよパターンでは、このルールの適用で不要な optional chain を削除することができますし、利用側の実装が変更になるため、型定義が変わったことに気づくこともできます。ただし、ESLint をソースコード修正の際にしか適用していない場合は、不要な optional chain は残り続ける可能性があります。ESLint を適用するタイミングには検討の余地があります。
② 型定義と動作が一致していないんだよパターンでは、このルールを適用すると、今まで発生しなかった実行時エラーが発生することになります。これを防ぐためには eslint-disable コメントで個別にルールを無効化する必要があります。
ただ、型定義と動作が一致していないという状態は健全なものではありません。動作に合わせて型定義を変更するのが第一義で、個別のルール無効化はイレギュラーケースと考えるのが良いと思います。
おわりに
とても便利な optional chain ですが、時々不要そうなものを見つけては「これ要るのかなぁ」とストレスを貯めていました。@typescript-eslint/no-unnecessary-condition で型に応じて自動的に変換できることはわかったものの、一括変換はどんな影響があるのか怖いため、適用を躊躇していました。
そもそも修正しなくても動作に問題はなく、ソースコードの健全性を高めるためだけ、自分の気持を落ち着かせるためだけの事柄です。発生原因を考察することで少しだけ前向きになれましたが、既存のプロジェクトに適用するのは正直まだまだ怖いです。次のプロジェクトからはデフォルトルールに入れるぞ!と強く誓ってこの記事を締めたいと思います。