TypeScriptには、添え字アクセスに対して型注釈をつけるための Index Signatureという仕組みがあります。
これについて、某ライブラリの型定義でギョッとする使われ方をしていて、修正PRを出してアクセプトしていただいたのですが、ちょっと面白い(恐ろしい)事例だったので紹介。
インデックスシグネチャとは
プロパティへの添え字アクセス(C# でいうところのインデクサー)に対する型情報を定義します。
たとえば、数値をキーにして文字列を保持する連想配列であれば、こんなふうに定義できます。
interface NumStrDictionary {
[key: number]: string;
}
使用イメージ。
//初期化
const dic: NumStrDictionary = {
1: "いち",
2: "に"
}
//更新
dic[10] = "じゅう";
//取得
console.log(dic[1]); //いち
console.log(dic[10]); //じゅう
console.log(dic[3]); //undefined
キーがstringの場合、ドットアクセスが可能
js使いにとっては「そりゃそうでしょ」という感じなのかもしれませんが、このインデックスシグネチャのキーがstringである場合、 .キー名
という書式でのアクセスが可能なんです(TypeScript 2.2以降:参考)
interface StrStrDictionary {
[key: string]: string;
}
const dic: StrStrDictionary = {}
dic.hoge = "ほげ";
dic["huga"] = "ふが";
console.log(dic["hoge"]); //ほげ
console.log(dic.huga); //ふが
これが恐ろしいことに・・・。
存在しないプロパティに対するコンパイルエラーが効かなくなる
前の例を見てもらえればわかるとおり、 .キー名
で任意のキーに対する値を取得/設定できるということはつまり、 stringをキーとするインデックスシグネチャが定義されたinterfaceに対しては、タイポ等に対するコンパイルチェックが一切効かない ということです。
例のように、インデックスシグネチャのみが定義されたinterfaceであればさほど問題はないかもしれません。
しかし、私の遭遇した某ライブラリでは、そのコア機能の多くを定義するすべてのinterfaceの継承元interfaceとして、しかも最悪なことに any型を返すインデックスシグネチャが定義されていた ため、そのライブラリのほぼすべてのinterfaceにおいて、一切のコンパイルチェックが効かない状態となっていました。
interface Extendable {
// 拡張できるよ!みたいな意図だったらしい
[key: string]: any;
}
interface CoreFunctions extends Extendable {
func1(): void;
func2(): void;
}
const core: CoreFunctions = getCoreFunc();
core.func1();
core.func2();
core.func3(); //<- 書けちゃう
core.func4.aaa.bbb; //<- なんでも書けちゃう
しかし意外と気づかない
しかしこれが、こんな状態でも意外と気づかないものでした。
というのは、Visual Studio Codeのような高機能エディタであれば、存在するプロパティに対する補完自体は快適に機能するため、存在しないプロパティ名をタイプしてしまう機会というのが、まずほとんどないんですね。
なので気づかないでいると特にこわいのは、後からリファクタリングなどでプロパティ名を変更するようなケース。
どうせコンパイルエラーになるだろうと思っていたらならないので、実行時に初めて気がつくことになります。
というわけで、インデックスシグネチャを使用する際は注意しましょうという話でした。