Index signature(インデックス型)

JavaScript(TypeScript)のObjectは、他のJavaScriptオブジェクトへの参照を保持し、文字列でアクセスできます。

簡単な例:

let foo:any = {};
foo['Hello'] = 'World';
console.log(foo['Hello']); // World

キー"Hello"に文字列"World"を格納します。JavaScriptオブジェクトを保存できると言ったので、クラスインスタンスを保存してみましょう:

class Foo {
  constructor(public message: string){};
  log(){
    console.log(this.message)
  }
}

let foo:any = {};
foo['Hello'] = new Foo('World');
foo['Hello'].log(); // World

また、stringでアクセスできると言いましたね。もし他の何らかのオブジェクトをインデックスシグネチャに渡すと、JavaScriptのランタイムは.toStringを事前に呼び出し、その文字列を得ます。これは以下の通りです:

let obj = {
  toString(){
    console.log('toString called')
    return 'Hello'
  }
}

let foo:any = {};
foo[obj] = 'World'; // toString called
console.log(foo[obj]); // toString called, World
console.log(foo['Hello']); // World

インデックスを示す場所でobjが使われるたびにtoStringが呼び出されることに注意してください。

配列は若干異なります。number型のインデックスを使うと、JavaScript VMは、最適化を試みます(それが実際に配列であるか、格納された要素の構造がすべて一致しているかといったことに依存します)。なのでnumberはそれ自身、stringとは別の、正しいオブジェクトアクセサとみなされるべきです。以下は単純な配列の例です:

let foo = ['World'];
console.log(foo[0]); // World

それがJavaScriptです。ではTypeScriptでの優雅な取り扱い方を見ていきましょう。

TypeScriptのインデックスシグネチャ

JavaScriptはインデックスシグネチャにオブジェクトを使った場合、暗黙的にtoStringを呼び出します。そのため、TypeScriptは、初心者が落とし穴にはまるのを防ぐために、エラーを出します(私はいつもstackoverflowで落とし穴にはまるJavaScriptユーザーをたくさん見ています):

let obj = {
  toString(){
    return 'Hello'
  }
}

let foo:any = {};

// ERROR: the index signature must be string, number ...
foo[obj] = 'World';

// FIX: TypeScript forces you to be explicit
foo[obj.toString()] = 'World';

ユーザーに明示的にtoStringを使うことを強制する理由は、オブジェクトのデフォルトのtoString実装がかなりひどいためです。v8では常に[object Object]を返します:

let obj = {message:'Hello'}
let foo:any = {};

// ERROR: the index signature must be string, number ...
foo[obj] = 'World';

// Here is where you actually stored it!
console.log(foo["[object Object]"]); // World

もちろんnumberはサポートされています。理由は以下の通りです。

  1. すばらしい配列/タプルのサポートに必要です。

  2. あなたがobjとしてそれを使うとしても、デフォルトのtoString実装はまともです([object Object]ではありません)。

ポイント2を以下に示します。

console.log((1).toString()); // 1
console.log((2).toString()); // 2

だから、レッスン1:

TypeScriptのインデックスシグネチャはstringまたはnumberのいずれかでなければなりません。

参考まで: symbolsもまたTypeScriptでサポートされています。しかし、まだそこには行かないでください。小さな一歩からです。

インデックスシグネチャを宣言する

今まで私たちは、TypeScriptに私たちが望むことをさせるためにanyを使ってきました。私たちは、実際にはインデックスシグネチャを明示的に指定できます。例えば文字列を使ってオブジェクトに格納されているものが構造体{message: string}に従っていることを確認したいとします。これは{ [index:string] : {message: string} }の宣言で行うことができます。これは以下のとおりです:

let foo:{ [index:string] : {message: string} } = {};

/**
 * Must store stuff that conforms to the structure
 */
/** Ok */
foo['a'] = { message: 'some message' };
/** Error: must contain a `message` or type string. You have a typo in `message` */
foo['a'] = { messages: 'some message' };

/**
 * Stuff that is read is also type checked
 */
/** Ok */
foo['a'].message;
/** Error: messages does not exist. You have a typo in `message` */
foo['a'].messages;

ヒント: インデックスシグネチャの名前{ [index:string] : {message: string} }indexはTypeScriptにとっては意味がなく、可読性のためだけのものです。例えばもしそれがユーザー名であれば、コードを見る次の開発者のために{ [username:string] : {message: string} }と宣言することができます。

もちろん、numberインデックスもサポートされています。例:{ [count: number] : SomeOtherTypeYouWantToStoreEgRebate }

すべてのメンバはstringインデックスシグネチャに従わなければならない

stringインデックスシグネチャを持つと、それと同時に、すべての明示的なメンバもそのインデックスシグネチャに準拠している必要があります。これを以下に示します:

/** Okay */
interface Foo {
  x: number;
  y: number;
}
/** Error */
interface Bar {
  x: number;
  y: string; // ERROR: Property `y` must be of type number
}

これは、すべての文字列アクセスで同じ結果が得られるように安全性を提供するためです:

interface Foo {
  x: number;
}
let foo: Foo = {x:1,y:2};

// Directly
foo['x']; // number

// Indirectly
let x = 'x'
foo[x]; // number

限られた個数の文字列リテラルを使う

マップ型(Mapped Types)を使うことで、インデックス文字列が「リテラル文字型のユニオン型(Union)」のメンバであることを要求するようなインデックスシグニチャを作ることができます。

type Index = 'a' | 'b' | 'c'
type FromIndex = { [k in Index]?: number }

const good: FromIndex = {b:1, c:2}

// Error:
// Type '{ b: number; c: number; d: number; }' is not assignable to type 'FromIndex'.
// Object literal may only specify known properties, and 'd' does not exist in type 'FromIndex'.
const bad: FromIndex = {b:1, c:2, d:3};

辞書の語彙(vocabulary)の型を得るために、次のページで解説するkeyof typeofがしばしば共に使われます。

語彙の指定はジェネリクスを使うことで後回しにできます。

type FromSomeIndex<K extends string> = { [key in K]: number }

stringnumberインデクサの両方を持つ

これは一般的な使用例ではありませんが、TypeScriptコンパイラはこれをサポートしています。

しかし、stringインデクサはnumberインデクサよりも厳格であるという制約があります。これは意図的なものです。次のようなコードを可能にします:

interface ArrStr {
  [key: string]: string | number; // Must accommodate all members

  [index: number]: string; // Can be a subset of string indexer

  // Just an example member
  length: number;
}

デザインパターン:ネストされたインデックスシグネチャ

インデックスシグネチャを追加する際のAPIの考慮事項

JSコミュニティでは、通常、文字列インデクサを乱用するAPIをよく見かけるでしょう。例えばJSライブラリにおけるCSSの共通パターンです:

interface NestedCSS {
  color?: string;
  [selector: string]: string | NestedCSS;
}

const example: NestedCSS = {
  color: 'red',
  '.subclass': {
    color: 'blue'
  }
}

このように、文字列インデクサと有効な値を混在させないようにしてください。例えばオブジェクトに入れるときのタイプミスはキャッチされません:

const failsSilently: NestedCSS = {
  colour: 'red', // No error as `colour` is a valid string selector
}

代わりに、ネストを独自のプロパティに分離します。例えば、nest(またはchildrensubnodesなど)のような名前で宣言します:

interface NestedCSS {
  color?: string;
  nest?: {
    [selector: string]: NestedCSS;
  }
}

const example: NestedCSS = {
  color: 'red',
  nest: {
    '.subclass': {
      color: 'blue'
    }
  }
}

const failsSilently: NestedCSS = {
  colour: 'red', // TS Error: unknown property `colour`
}

インデックスシグネチャから特定のプロパティを除外する

場合によっては、プロパティを結合してインデックスシグニチャにしたいことがあります。これは推奨されておらず、上記のネストされたインデックスシグネチャのパターンを使用するべきです。

ただし、既存のJavaScriptをモデリングしている場合は、交差型(intersection type)で回避することができます。次に、交差型を使用せずに発生するエラーの例を示します。

type FieldState = {
  value: string
}

type FormState = {
  isValid: boolean  // Error: Does not conform to the index signature
}

交差型を使用した場合の回避策は次のとおりです。

type FieldState = {
  value: string
}

type FormState =
  { isValid: boolean }
  & { [fieldName: string]: FieldState }

あなたはそれを既存のJavaScriptオブジェクトに対して宣言することができますが、そのようなオブジェクトをTypeScriptで生成できないことに注意してください。

type FieldState = {
  value: string
}

type FormState =
  { isValid: boolean }
  & { [fieldName: string]: FieldState }


// Use it for some JavaScript object you are getting from somewhere 
declare const foo:FormState; 

const isValidBool = foo.isValid;
const somethingFieldState = foo['something'];

// Using it to create a TypeScript object will not work
const bar: FormState = { // Error `isValid` not assignable to `FieldState
  isValid: false
}

最終更新