はじめに
こんにちは!かずや(@bearone236)と申します。
今回は、TypeScript に搭載されている Index Signature (インデックスシグネチャ) について、その概念と型による挙動の違いを詳しく説明していきます。
インデックスシグネチャ(Index Signature) とは:
インデックスシグネチャ(Index Signature) は、TypeScript におけるオブジェクトの動的なプロパティの型定義を可能にする特殊な構文要素です。これは、オブジェクトやデータ構造に添字(インデックス)でアクセスするための仕組みを提供し、オブジェクトのプロパティ名とその型を動的に定義します。
端的に言うと、「どんな名前のプロパティだとしても受け入れるよ!」 という機能を持った構文です。
基本的な使用例
インデックスシグネチャは基本的に、オブジェクト型の中で [key名: string]: 型名
で記載します。
type ItemsType = {
[key: string]: string;
};
const items: ItemsType = {
desk: "wooden",
phone: "plastic",
glasses: "metal",
bag: "leather",
};
console.log(items["bag"]); // 出力結果: leather
実際の業務等では、インデックスシグネチャ(Index Signature)は動的なデータ構造を扱う際に非常に役立つ要素と言われています。
例を挙げると、ユーザーのデータや設定オプションといった、キーが事前に定義できないオブジェクトの型を定義する場合に使用されます。
例えば、異なるユーザー属性を持つオブジェクトを管理する際に、次のように使用することが可能です。
interface UserAttributes {
[key: string]: string | number;
}
function displayUser(user: UserAttributes) {
for (let key in user) {
console.log(`${key}: ${user[key]}`);
}
}
const user: UserAttributes = {
name: "Kazuya",
age: 21,
occupation: "WebEngineer",
};
displayUser(user);
この例では、UserAttributes
インターフェースは任意の数のプロパティを持つことができ、それぞれのプロパティは文字列または数値の型を持つことができます。
このコードによって異なるユーザー属性を柔軟に扱うことが可能です。
TypeScript の型制約について
TypeScript のインデックスシグネチャは、キーとして string
, number
, または symbol
型をサポートします。これは、JavaScript オブジェクトが内部的にこれらの型のキーしか持たないためです。そのため、他の型をインデックスシグネチャに定義してしまうとコンパイルエラーを引き起こします。
使用技術スタック
TypeScript: version5.3.2
不思議な挙動の紹介
ある実装を行っている際に不思議な挙動を発見したため共有します。
※ 下記の記載及び実装は、日常的に TypeScript を使用しているエンジニアの視点から見ると少々異質な記載かもしれませんが、ご理解・ご了承くださいますと幸いです。
「型の中で Index Signature を定義する場合」と「ある関数の引数で Index Signature を定義する場合」において、type
と interface
で異なる挙動が発生!
大きく分けて 2 つの例を示します。
-
型定義内でのインデックスシグネチャ
-
関数の引数でのインデックスシグネチャ
【① 型定義内でのインデックスシグネチャ】
▶️ 型エイリアス (type)の場合
function TTest(test1: TypeTest) {
return console.log(test1);
}
type TypeTest = {
[key: string]: string;
};
const test1: TypeTest = {
text: "test",
};
TTest(test1); // No Error
= エラーなし
▶️ Interface 型の場合
function ITest(test1: InterfaceTest) {
return console.log(test1);
}
interface InterfaceTest {
[key: string]: string;
}
const test1: InterfaceTest = {
text: "test",
};
ITest(test1); // No Error
= エラーなし
【② 関数の引数でのインデックスシグネチャ】
▶️ 型エイリアス (type)の場合
function TTest(obj: { [key: string]: string }) {
return console.log(obj);
}
type TypeObjectTest = {
text: string;
};
const test2: TypeObjectTest = {
text: "test",
};
TTest(test2); // No Error
= エラーなし
▶️ 🚨Interface型の場合🚨
function ITest(obj: { [key: string]: string }) {
return console.log(obj);
}
interface InterfaceObjectTest {
text: string;
}
const test2: InterfaceObjectTest = {
text: "test",
};
ITest(test2); // Error
= エラーが発生!!!
「BugFiler: Argument of type 'InterfaceObjectTest' is not assignable to parameter of type '{ [key: string]: string; }'.」
「Index signature for type 'string' is missing in type 'InterfaceObjectTest'.」
こちらのエラーは、InterfaceObjectTest
型が関数に求められるインデックスシグネチャを満たしていないことを表しています。
なぜ型宣言内でインデックスシグネチャを定義した際は両方の型定義ともエラーを出力しなかったのにも関わらず、関数の引数にインデックスシグネチャを定義した際のみ Interface 型だけエラーが見られるのでしょうか。
議論されていたブログ URL
・実際に、上記の問題に関して GitHub Issue で議論が巻き起こっていました。
Index Signature に関するGithub Issue
解決策発見
下記の参考サイトにて解決策が記載されていたので紹介します。
【解決策】 スプレッド構文(...)を使用した型互換性の解決
function ITest(obj: { [key: string]: string }) {
return obj;
}
interface InterfaceObjectTest {
text: string;
}
const IObject: InterfaceObjectTest = {
text: "test",
};
console.log(ITest({ ...IObject })); // ← スプレッド構文による記載に変更
このコードでは、スプレッド構文 ({ ...IObject }) を使用して InterfaceObjectTest 型のオブジェクトから新しいオブジェクトを生成します。このシャローコピーの機能によって、以下の利点を得ることができます:
- 型互換性の確保: 新しいオブジェクトは { [key: string]: string } のインデックスシグネチャに適合し、ITest 関数の型要求を満たします。
- 元の型の制約からの解放: スプレッド構文により生成された新しいオブジェクトは、元の InterfaceObjectTest インターフェースの型制約から解放されます。
これらの方法によって、ITest 関数に対して型エラーなしで IObjectを渡すことが可能になり、TypeScriptの型システムにおける一般的な互換性問題を解決します。
まとめ
TypeScript の型エイリアス(type)とインターフェース(interface)は、インデックスシグネチャを含む多くの用途で似たように機能します。
主な違いとしては、インターフェースが拡張可能であるのに対し、型エイリアスはユニオンや交差型と組み合わせるのに適している点です。
実際の使用はプロジェクトのニーズや設計に応じて決定されるべきですが、ぜひのインデックスシグネチャ(Index Signature)知識を持ち帰り、活用してみてください!
参考文献