aknow2

自分の興味のある事を連々と(プログラミング、モバイルアプリ、プログラミング教育)

Typescript ブラケット記法(Object[key])でno index signatureエラーをtype safeに解決したい。

TypescriptでObjectに対して[文字列]でアクセスするブラケット記法を用いると発生するエラー。
例えば、この様に書くとエラーが出てきてコンパイルが通りません。

interface ISomeObject {
  firstKey:      string;
  secondKey:     string;
}

const obj = {
  firstKey: "a",
  secondKey: "b",
} as ISomeObject;

const key: string = 'secondKey';
const secondValue: string = obj[key];  // Element implicitly has an 'any' type because type '() => void' has no index signature.

ブラケット記法を使いたいため、keyに'secondKey'という文字列を指定しています。obj[key]でプロパティにブラケット記法でアクセスするとコンパイラは何型が返ってくるか推定出来ずにエラーとなります。
この現象に対して色々と回避方法があるので、まとめてみました。

その1・コンパイラオプションで蹴る。

↓をtsconfigに書き加えればOK。

--suppressImplicitAnyIndexErrors

でも、コレは使いたくない。  
コレを指定するとブラケット記法でプロパティにアクセスした場合にany型を許容することになります。そうなると何型の値が返ってくるか分かりません。つまり、type safeで無くなってしまう。type safeが利点であるTypescriptでanyを許容するのはいただけない。もし、keyをtypoしてしまったら実行するまでその間違いに気づけません。

その2・ interfaceを準備して[key: string]: <型>を追加する

次の解決策、ブラケット記法でアクセスした場合に返ってくる型をコンパイラに教えてあげる。
例えば、interfaceのプロパティに[key: string]: stringを追加すると、ブラケット記法でアクセスした場合、stringが返ってくるという意味になります。 こんな感じです。

interface ISomeObject {
    firstKey:      string;
    secondKey:     string;
    [key: string]: string; // <-この行を追加!
}

const obj = {
  firstKey: "a",
  secondKey: "b",
} as ISomeObject;

const key: string = 'secondKey';
const secondValue: string = obj[key]; 

だが、コレにもちょっと問題がある。
例で示したinterfaceに存在するプロパティはstring型のみ、普通は様々な型が入りますよね。
複数の型が存在する場合は複数の型を指定すれば一応、コンパイルは通ります。
例えば、string,number,booleanがinterfaceのプロパティに存在する場合、、、

interface ISomeObject {
    firstKey:      string;
    secondKey:     number;
    thirdKey: boolean
    [key: string]: string|boolean|number;  //<-or条件で型を指定 
}

const key: string = 'secondKey';
const secondValue = obj[key]; // secondValue => string|boolean|number型

コンパイルは通りますが, secondValueはnumber型ではなく、string|boolean|number型となってしまいます。
結局、ブラケット記法でアクセスすると何型が返ってくるか分かりません。その1の解決策と同じ様な状況になってしまいます。
この方法を取れば、影響範囲はinterfaceを実装しているオブジェクトのみで、型もある程度は推定出来るので、その1の方法よりかは幾分マシですが完璧とは言えません。

その3・keyofを使う。

前提を一つ壊すことになりますが、そもそも、keyをstring型にしているのが間違いだよ!という話。
const key: stringとしていた箇所をconst key: keyof ISomeObjectに変えてみましょう。
こうする事でkeyはISomeObjectのプロパティであるfirstKey,secondKey,thirdKeyのいずれかの文字列が入るstring literal typeに変身します。

interface ISomeObject {
  firstKey:      string;
  secondKey:     number;
  thirdKey:      boolean;
}

const obj = {
  firstKey: "a",
  secondKey: 2,
  thirdKey: false
} as ISomeObject;

const key: keyof ISomeObject  = 'secondKey';
const secondValue = obj[key]; // secondValueがnumber型に!

こうすれば、secondValueがちゃんとnumber型として認識してくれます!
また、keyをtypoしてもコンパイラが間違っている事を教えてくれます。
こんな感じに、、、。

const key: keyof ISomeObject  = 'scdKey';  //  Type '"sndKey"' is not assignable to type '"secondKey" | "firstKey" | "thirdKey"'.

また、ジェネリクスを使って、type safeにブラケット記法が使える関数を作るとこんな感じになります。

const accessByBracket = <S, T extends keyof S>(obj: S, key: T) => {
  return obj[key];
};

const obj = {
  firstKey: "a",
  secondKey: 2,
  thirdKey: false
} as ISomeObject;

const value = accessByBracket(obj, 'secondKey'); // value => 2

まとめ

コンパイルオプションをいじったり、interfaceにプロパティを追加する前にkeyofを上手く使って型安全な設計に出来ないか検討しましょう。