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での優雅な取り扱い方を見ていきましょう。
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
インデックスシグネチャを持つと、それと同時に、すべての明示的なメンバもそのインデックスシグネチャに準拠している必要があります。これを以下に示します:/** 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 }
これは一般的な使用例ではありませんが、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
(またはchildren
や subnodes
など)のような名前で宣言します: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
}
最終更新 8mo ago