TypeScriptのnoUncheckedIndexedAccessとそれに関連して欲しいかもしれない型

TypeScriptのnoUncheckedIndexedAccessと、それに関連して欲しいかもしれない型を書きます。

noUncheckedIndexedAccess について

まずtsconfigのnoUncheckedIndexedAccessを有効にすると、インデックスアクセスの返り値の型にundefinedが含まれるようになります。

const arr: number[] = [1, 2, 3];
const e = arr[0];  // この型は number | undefined になる

配列の境界外(またはsparse array(疎な配列)のemptyの要素)にアクセスするとundefinedが返るためです。

実際に有効にしてみると型にundefinedが含まれることによりかなりの数のエラーが発生して、直すのも大変です。

オプションを導入するissueでも、まともな開発者体験を得るにはTypeScriptに新たなControl Flow Analysis (CFA) を追加しないといけなくて大変と書いてありました。

Suggestion: option to include undefined in index signatures · Issue #13778 · microsoft/TypeScript · GitHub

実際にエラーが出たところを見ると、確かに要素数が0個のときを考慮していなかったかもというところがあって、実際に有効な場面もありそうでした。 (実装時の注意や、ユニットテストで見つけるべきなのかもしれないですが)

「n個以上の要素が含まれる配列」型

一応以下のようにTypeScriptで「n個以上の要素が含まれる配列」というのを表現する方法はあるようなので、noUncheckedIndexedAccessで出たエラーの一部はそれを使って解決できました。

TypeScript array with minimum length - Stack Overflow

type arrMin1Str = [string, ...string[]]; // The minimum is 1 string.

しかし、mapした後には普通の配列型になってしまう(mapで要素数は変わらないはず)、型を書くのが少し面倒、リテラルから自動で推論されないといった点はあるため、TypeScript組み込みでサポートしたほうが便利そうです。

配列の長さと関連付いた数値型

Array.lengthプロパティの返り値は単なる数値型なので、よくあるforループで配列の要素にアクセスするパターンでundefinedが含まれてしまいます。

const arr: number[] = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  const e = arr[i];  // number | undefined
}

lengthの返り値の型を元の配列の長さと紐付けられればなんとかなるかもしれません。

他の言語では配列のアクセスはどうなのか

C#Javaは言うまでもなく、OCamlHaskellなどの静的型付けの関数型言語でも配列の範囲外アクセスは基本的に例外で表現します。

もちろん関数型言語のユーザは型にうるさい人が多いので、例外を投げる代わりに型で失敗を表現する方法も用意されているようです。(配列というかリストですが、OCamlではList.nthの代わりにList.nth_opt、Haskellでは!!演算子の代わりにlensライブラリを使うなど)

ただ、関数型言語の場合はループを使うことはそこまで多くなくて、map, filter, foldlなどでリストを処理することが多いように思います。

余談

調べていてこんな記事を見つけました。

exactOptionalPropertyTypes によせて - Object.create(null)

プロパティが減る方向の暗黙のキャストとオプショナルプロパティが組み合わさると、静的な型とは異なる実行時の型のデータが入ることがあるということです。

部分型を許さない型があるといいんですかね。

TypeScript をより安全に使うために まとめ - Object.create(null)

このあたりも知らなかったので参考になりました。 オブジェクトをMapとして使うのを避けるのと、積極的にreadonlyを付けていこうと思います。

スプレッド構文を後ろで使うと静的に見えないプロパティで上書きされるなど、静的に見れそうなところもありました。

余談 整数型を考える

TypeScriptのnumberはJavaScriptの数値型に対応するので、浮動小数点数を表します。 整数のみを表す型は標準では存在しません。

TypeScriptの型定義から、このフィールドは整数のみが入るのか、小数も入るのかがわからないので、別途コードや資料を確認したくなることがありました。

整数型があるとその情報を表現できるし、小数が来ないことで実装時に考えることが減るかもしれません。

感想

noUncheckedIndexedAccessの個人的な感想としては、なんかいい感じの型で検証できるようにして、いわゆるnull安全くらいには自然に使えるようになったらいいなと思います。 型検査の処理がかなり複雑化するかもしれないですが。