- TypeScript
TypeScript インデックスシグネチャの使い方|Recordとの違い
インデックスシグネチャとは、{ [key: string]: number } のように「キーの型」と「値の型」だけを決めて、キーの名前を決めないオブジェクト型の書き方です。 集計結果や辞書のように、キーが実行時に決まるオブジェクトに使います。Record<string, number> と書いた型と同じものです。
この記事は [key: string] の書き方そのものに絞り、キーに書ける型、固定プロパティとの併用、存在しないキーを読んだときの型、2つのコンパイラオプション、readonly、interface と type の違いを、tsc のエラー全文つきで確かめます。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / TypeScript 7.0.2(npx tsc --version の表示は Version 7.0.2)。エラー文は、ことわりのない限り npx tsc --noEmit --strict の出力です。
インデックスシグネチャの早見表
キーに書ける型
| キーの型 | 書き方の例 | 書けるか | エラー |
|---|---|---|---|
| string | { [key: string]: number } |
書ける | なし |
| number | { [index: number]: string } |
書ける | なし |
| symbol | { [key: symbol]: boolean } |
書ける | なし |
| テンプレートリテラル型(パターン) | { [key: `data-${string}`]: string } |
書ける | なし |
| 上の型どうしのユニオン | { [key: string | number]: unknown } |
書ける | なし |
| string の別名(type Id = string) | { [key: Id]: number } |
書ける | なし |
| リテラルのユニオン | { [key: "a" | "b"]: number } |
書けない | TS1337 |
| リテラル1つ | { [key: "a"]: number } |
書けない | TS1337 |
| 型引数 | { [key: K]: number } |
書けない | TS1337 |
| enum | { [key: Color]: number } |
書けない | TS1337 |
| boolean・bigint | { [key: boolean]: number } |
書けない | TS1268 |
| 省略可能の印つき | { [key?: string]: number } |
書けない | TS1019 |
やりたいことから引く
| やりたいこと | 書き方 |
|---|---|
| キーが実行時に決まる辞書 | { [key: string]: V } または Record<string, V> |
| キーが決まった数種類 | Record<"a" | "b", V> または { [K in "a" | "b"]: V } |
| 一部は固定、残りは自由 | 索引の値の型を、固定プロパティの型をすべて含む型にする |
| 存在しないキーを読んだら undefined 込みにしたい | noUncheckedIndexedAccess を有効にする |
| 索引由来のキーをドットで読めないようにしたい | noPropertyAccessFromIndexSignature を有効にする |
| 書き換えを止めたい | { readonly [key: string]: V } |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript インデックスシグネチャの使い方|Recordとの違い」
基本の書き方と読み方
type Stock = { [itemName: string]: number };
// ~~~~~~~~ ~~~~~~ ~~~~~~
// 名前(任意) キーの型 値の型
const stock: Stock = { pen: 12, note: 5 };
stock["eraser"] = 3; // 新しいキーを足せる
| 部分 | 意味 |
|---|---|
itemName |
説明用の名前。key・index など自由に付けられ、型の動きには影響しない |
string |
キーの型 |
number |
そのキーで読み書きする値の型 |
interface でも同じ書き方ができます。
interface Stock {
[itemName: string]: number;
}
キーに書ける型を tsc で確かめる
早見表の内容を1つのファイルで確かめました。
// keys.ts
// キーに書ける型
type ByString = { [key: string]: number };
type ByNumber = { [index: number]: string };
type BySymbol = { [key: symbol]: boolean };
type ByPattern = { [key: `data-${string}`]: string };
type ByUnionOfPrimitives = { [key: string | number]: unknown };
type ByBoth = { [key: string]: number; [key: symbol]: string };
// キーに書けない型
type ByLiteralUnion = { [key: "a" | "b"]: number };
type ByLiteral = { [key: "a"]: number };
type ByBoolean = { [key: boolean]: number };
type ByGeneric<K extends string> = { [key: K]: number };
type ByEnumKey = { [key: Color]: number };
enum Color { Red = "red", Blue = "blue" }
type ByOptional = { [key?: string]: number };
type ByBigint = { [key: bigint]: number };
type ByAlias = { [key: Id]: number };
type Id = string;
$ npx tsc --noEmit --strict keys.ts
keys.ts(10,26): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
keys.ts(11,21): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
keys.ts(12,21): error TS1268: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
keys.ts(13,39): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
keys.ts(14,21): error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
keys.ts(16,25): error TS1019: An index signature parameter cannot have a question mark.
keys.ts(17,20): error TS1268: An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
2〜7行目(string・number・symbol・テンプレートリテラル型・それらのユニオン・併記)と、18行目の「string の別名」にはエラーが出ていません。
リテラルのユニオンは Mapped Types か Record で書く(TS1337)
TS1337 のメッセージは「代わりに mapped object type を検討してください」と案内しています。キーが決まった数種類のときは、in を使う書き方(Mapped Types)か Record に変えます。
// 直す前:TS1337
type ByLevel = { [key: "low" | "mid" | "high"]: number };
// 直した後:どちらも同じ型
type Level = "low" | "mid" | "high";
type ByLevel1 = { [K in Level]: number };
type ByLevel2 = Record<Level, number>;
const lv: ByLevel1 = { low: 1, mid: 2 };
misc.ts(20,7): error TS2741: Property 'high' is missing in type '{ low: number; mid: number; }' but required in type 'ByLevel1'.
こちらは3つのキーがすべて必須になるので、書き漏れ(high)を知らせてくれます。索引シグネチャは「どんなキーでもよい」、Mapped Types と Record は「このキーをすべて持つ」 という違いです。
Record との違い
Record<string, V> と { [key: string]: V } は同じ型です。次のコードで、2つが一致することを確かめました(Equal は2つの型が一致すれば true 型になる検証用の型で、不一致なら TS2322 で止まります)。
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Dict = { [key: string]: number };
const k5: Equal<Dict, Record<string, number>> = true; // 通る
違いが出るのは、書ける内容の幅です。
| 観点 | 索引シグネチャ | Record |
|---|---|---|
| キーが string・number・symbol・パターン | 書ける | 書ける |
| キーがリテラルのユニオン | TS1337 | 書ける(全キーが必須になる) |
| 固定プロパティと同じ場所に書く | 書ける | 交差型(&)で足す |
| キーに説明用の名前を付ける | 付けられる([itemName: string]) |
付けられない |
| readonly | readonly [key: string]: V |
Readonly<Record<string, V>> |
| interface の中に書く | 書ける | 書けない(extends で継承する) |
キーが自由な辞書ならどちらでも同じ、キーが決まっているなら Record、固定プロパティと混ぜたい・キーに名前を付けて意図を伝えたいなら索引シグネチャ、と選べます。
固定プロパティと併用するときの決まり(TS2411)
索引シグネチャと名前の決まったプロパティは、同じ型の中に書けます。ただし、固定プロパティの型は、索引の値の型に収まっている必要があります。
// fixed.ts
// 固定プロパティと併用する
type Headers1 = {
[key: string]: string;
contentLength: number;
accept: string;
};
type Headers2 = {
[key: string]: string | number;
contentLength: number;
accept: string;
};
type Headers3 = {
[key: string]: string;
note?: string;
};
interface WithMethod {
[key: string]: string;
toText(): string;
}
fixed.ts(4,3): error TS2411: Property 'contentLength' of type 'number' is not assignable to 'string' index type 'string'.
fixed.ts(16,3): error TS2411: Property 'note' of type 'string | undefined' is not assignable to 'string' index type 'string'.
fixed.ts(21,3): error TS2411: Property 'toText' of type '() => string' is not assignable to 'string' index type 'string'.
| 型 | 結果 | 理由 |
|---|---|---|
| Headers1 | TS2411 | contentLength が number で、索引の string に収まらない |
| Headers2 | 通る | 索引を string | number に広げた |
| Headers3 | TS2411 | 省略可能なプロパティは string | undefined になる |
| WithMethod | TS2411 | メソッドも「関数型のプロパティ」として数えられる |
obj["contentLength"] と obj.contentLength は同じ値を指すので、両方の型がそろっている必要がある、というのが理由です。
直し方の選択肢
| 直し方 | 書き方 | 向いている場面 |
|---|---|---|
| 索引の値の型を広げる | [key: string]: string | number |
固定プロパティの型が少ない |
| 省略可能な分を足す | [key: string]: string | undefined |
省略可能なプロパティがある |
| 自由なキーを1つのプロパティにまとめる | { contentLength: number; extra: { [key: string]: string } } |
固定部分と自由部分をはっきり分けたい |
| キーをパターンにする | [key: `x-${string}`]: string |
自由なキーに接頭辞などの決まりがある |
4つ目の「パターンにする」は、固定プロパティの名前がパターンに当てはまらなければ TS2411 の対象になりません。
交差型で足す書き方は、読むときだけ使える
& でつなぐと定義時のエラーは出ませんが、オブジェクトリテラルを代入する場面で止まります。
type Mixed = { contentLength: number } & { [key: string]: string };
const m: Mixed = { contentLength: 10, accept: "text/html" };
declare const m2: Mixed;
const len: number = m2.contentLength;
const acc: string = m2.accept;
fixed.ts(25,7): error TS2322: Type '{ contentLength: number; accept: string; }' is not assignable to type 'Mixed'.
Type '{ contentLength: number; accept: string; }' is not assignable to type '{ [key: string]: string; }'.
Property 'contentLength' is incompatible with index signature.
Type 'number' is not assignable to type 'string'.
読み取り(27・28行目)は通りましたが、値を作る25行目がエラーです。値を自分で作るなら、上の表の直し方を選ぶほうが扱いやすくなります。
存在しないキーを読んだときの型:noUncheckedIndexedAccess の有無を比べる
索引シグネチャは「どんなキーでも値がある」と宣言する書き方です。そのため、既定では、存在しないキーを読んでも型は number のまま になります。
// stock.mts
type Stock = { [itemName: string]: number };
const stock: Stock = { pen: 12, note: 5 };
const pen = stock["pen"];
const eraser = stock["eraser"]; // 存在しないキー
console.log(pen.toFixed(1));
console.log(eraser);
console.log(eraser.toFixed(1));
オプションなし:型チェックは通り、実行すると止まる
$ npx tsc --noEmit --strict --target es2022 --module nodenext stock.mts
$ echo $?
0
$ node stock.mts
12.0
undefined
file:///home/taro/work/stock.mts:10
console.log(eraser.toFixed(1));
^
TypeError: Cannot read properties of undefined (reading 'toFixed')
(パスの表示は環境に合わせて置き換えています。)
strict を付けていても tsc はエラーを出さず、実行時に undefined のメソッド呼び出しで止まりました。
オプションあり:コンパイル時に知らせてくれる
$ npx tsc --noEmit --strict --target es2022 --module nodenext --noUncheckedIndexedAccess stock.mts
stock.mts(8,13): error TS18048: 'pen' is possibly 'undefined'.
stock.mts(10,13): error TS18048: 'eraser' is possibly 'undefined'.
noUncheckedIndexedAccess を有効にすると、索引シグネチャ経由で読んだ値の型に undefined が加わります。 実際に値がある pen も対象です。型の上では、どのキーに値があるかを区別できないためです。
オンとオフで変わる型の一覧
次の表は、各行の式の型を Equal 型で確かめた結果です。
| 式 | オフ | オン |
|---|---|---|
stock["pen"](索引シグネチャ) |
number |
number | undefined |
stock.pen(ドットで読む) |
number |
number | undefined |
stock[key](key は string 型の変数) |
number |
number | undefined |
Record<string, number> の rec["pen"] |
number |
number | undefined |
string[] の list[0] |
string |
string | undefined |
タプル [string, number] の tuple[0] |
string |
string |
Record<"pen" | "note", number> の ["pen"] |
number |
number |
for (const k in stock) の中の stock[k] |
number |
number | undefined |
if ("pen" in stock) の中の stock["pen"] |
number |
number |
if (key in stock) の中の stock[key] |
number |
number | undefined |
キーが決まっている型(タプル、リテラルキーの Record)は影響を受けません。 また、リテラルのキーを in で確かめた後は undefined が外れますが、string 型の変数で確かめた場合は外れませんでした。
書き込みの型は変わりません。オンにしても stock["eraser"] = undefined は次のエラーになります。
t-on.ts(29,1): error TS2322: Type 'undefined' is not assignable to type 'number'.
オンにしたときの書き方
// stock-fix.mts
type Stock = { [itemName: string]: number };
const stock: Stock = { pen: 12, note: 5 };
// 1) undefined を確かめてから使う
const eraser = stock["eraser"];
if (eraser !== undefined) {
console.log(eraser.toFixed(1));
} else {
console.log("eraser: 登録なし");
}
// 2) ?? で既定値を決める
const count = stock["eraser"] ?? 0;
console.log(count.toFixed(1));
// 3) リテラルのキーを in で確かめてから読む
if ("pen" in stock) {
console.log(stock["pen"].toFixed(1));
}
// 4) Object.entries で回す(値は number のまま)
for (const [item, n] of Object.entries(stock)) {
console.log(item, n.toFixed(0));
}
$ npx tsc --noEmit --strict --target es2022 --module nodenext --noUncheckedIndexedAccess stock-fix.mts
$ node stock-fix.mts
eraser: 登録なし
0.0
12.0
pen 12
note 5
| 場面 | 書き方 |
|---|---|
| 1件読む | 変数に受けて !== undefined で確かめる |
| なければ既定値でよい | ?? で既定値を決める |
| 全件を処理する | Object.entries や Object.values で回す |
| 名前のわかっているキー | in で確かめる |
オプションを使わず、型の側で undefined を含める方法
値の型に undefined を書いておく方法もあります。
type Stock = { [itemName: string]: number | undefined };
| 方法 | 読み取り | 書き込み |
|---|---|---|
| noUncheckedIndexedAccess | undefined 込み | number だけ |
値の型に | undefined を書く |
undefined 込み | undefined も代入できる |
オプションは全体に効き、書き込み側の型を保てます。型に書く方法は、その型だけに効かせたいときに選べます。
noPropertyAccessFromIndexSignature:索引由来のキーをドットで読ませない(TS4111)
固定プロパティと索引シグネチャが混ざった型では、env.API_URL と書いたとき、それが「宣言されたプロパティ」なのか「索引で受けているだけ」なのかが見た目でわかりません。このオプションを有効にすると、索引由来のキーはブラケットで書く決まりになります。
// dot.ts
type Env = {
NODE_ENV: string; // 固定プロパティ
[name: string]: string; // それ以外
};
declare const env: Env;
const a = env.NODE_ENV; // 固定プロパティ:ドットで読める
const b = env.API_URL; // 索引シグネチャ由来:ドットで読むと TS4111
const c = env["API_URL"]; // ブラケットなら読める
env.DEBUG = "1"; // 書き込みも同じ扱い
env["DEBUG"] = "1";
$ npx tsc --noEmit --strict dot.ts
$ echo $?
0
$ npx tsc --noEmit --strict --noPropertyAccessFromIndexSignature dot.ts
dot.ts(8,15): error TS4111: Property 'API_URL' comes from an index signature, so it must be accessed with ['API_URL'].
dot.ts(10,5): error TS4111: Property 'DEBUG' comes from an index signature, so it must be accessed with ['DEBUG'].
| 書き方 | オフ | オン |
|---|---|---|
env.NODE_ENV(固定プロパティ) |
通る | 通る |
env.API_URL(索引由来・ドット) |
通る | TS4111 |
env["API_URL"](索引由来・ブラケット) |
通る | 通る |
ドットで書けるのは宣言されたプロパティだけになるので、つづりの違いに気づきやすくなります。 公式リファレンスはこの設定の目的を、プロパティが存在することへの確信の度合いを、呼び出しの書き方で示せるようにすること、と説明しています。
2つのオプションの比較と設定
| オプション | 変わること | 出るエラー | strict だけで有効になるか |
|---|---|---|---|
| noUncheckedIndexedAccess | 読んだ値の型に undefined が加わる | TS18048・TS2532 | ならない |
| noPropertyAccessFromIndexSignature | 索引由来のキーをドットで書けなくなる | TS4111 | ならない |
「strict だけで有効になるか」の列は、--strict だけを付けた tsc で stock.mts と dot.ts がエラーなしになったことから確かめています。使うときは tsconfig.json に個別に書きます。
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"strict": true,
"noEmit": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true
},
"include": ["*.mts"]
}
readonly な索引シグネチャ(TS2542)
先頭に readonly を付けると、索引経由の書き込みと削除がエラーになります。
// ro.ts
type Labels = { readonly [code: string]: string };
const labels: Labels = { ok: "完了", ng: "要確認" };
const x = labels["ok"]; // 読み取りは可
labels["ok"] = "済"; // 書き込み
delete labels["ok"]; // 削除
const list: ReadonlyArray<string> = ["a"];
list[0] = "b";
// Readonly<Record<string, string>> も同じ意味
const labels2: Readonly<Record<string, string>> = { ok: "完了" };
labels2["ok"] = "済";
// 入れ子の中までは readonly にならない
type Nested = { readonly [code: string]: { text: string } };
const nested: Nested = { ok: { text: "完了" } };
nested["ok"]!.text = "済";
ro.ts(5,1): error TS2542: Index signature in type 'Labels' only permits reading.
ro.ts(6,8): error TS2542: Index signature in type 'Labels' only permits reading.
ro.ts(9,1): error TS2542: Index signature in type 'readonly string[]' only permits reading.
ro.ts(13,1): error TS2542: Index signature in type 'Readonly<Record<string, string>>' only permits reading.
- 書き込み(5行目)と delete(6行目)の両方が止まります。
- 9行目のとおり、
ReadonlyArray<string>(readonly string[])も「readonly な number の索引シグネチャ」として同じエラーになります。 - 18行目にはエラーが出ていません。readonly が効くのは索引の1段目だけ で、値のオブジェクトの中身は書き換えられます。中まで止めたいときは、値の型にも readonly を付けます。
number キーと string キーを併記するときの決まり(TS2413)
1つの型に複数の索引シグネチャを書けます。そのとき、number の索引の値の型は、string の索引の値の型に収まっている必要があります。
// numstr.ts
// number キーと string キーを併記する
type A = {
[index: number]: string;
[key: string]: string | number;
};
type B = {
[index: number]: string | number;
[key: string]: string;
};
type C = {
[key: string]: number;
[key: `id_${string}`]: string;
};
type Dup = {
[key: string]: number;
[name: string]: number;
};
numstr.ts(7,3): error TS2413: 'number' index type 'string | number' is not assignable to 'string' index type 'string'.
numstr.ts(12,3): error TS2413: '`id_${string}`' index type 'string' is not assignable to 'string' index type 'number'.
numstr.ts(15,3): error TS2374: Duplicate index signature for type 'string'.
numstr.ts(16,3): error TS2374: Duplicate index signature for type 'string'.
| 型 | 結果 | 理由 |
|---|---|---|
| A | 通る | number 側(string)が string 側(string | number)に収まる |
| B | TS2413 | number 側のほうが広い |
| C | TS2413 | パターンのキーも string の一種なので、同じ決まりが当てはまる |
| Dup | TS2374 | 同じキーの型の索引は1つだけ。引数名を変えても同じ扱い |
公式ハンドブックは理由を、「number で添字アクセスすると、JavaScript は実際にはそれを string に変換してからオブジェクトを引く。100 で引くのは "100" で引くのと同じなので、2つは一貫している必要がある」と説明しています。
number・symbol・パターンのキーで読めるもの
type NumDict = { [index: number]: string };
type SymDict = { [key: symbol]: boolean };
type DataDict = { [key: `data-${string}`]: string };
const nd: NumDict = { 0: "a", 1: "b" };
const n0 = nd[0];
const n1 = nd["1"];
const n2 = nd["one"];
const tag = Symbol("tag");
const sd: SymDict = { [tag]: true };
const s0 = sd[tag];
const s1 = sd["tag"];
const dd: DataDict = { "data-id": "7", "data-role": "admin" };
const dd2: DataDict = { "data-id": "7", title: "x" };
const d0 = dd["data-id"];
const d1 = dd["title"];
misc.ts(26,15): error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.
misc.ts(32,12): error TS7053: Element implicitly has an 'any' type because expression of type '"tag"' can't be used to index type 'SymDict'.
Property 'tag' does not exist on type 'SymDict'.
misc.ts(36,41): error TS2353: Object literal may only specify known properties, and 'title' does not exist in type 'DataDict'.
misc.ts(38,12): error TS7053: Element implicitly has an 'any' type because expression of type '"title"' can't be used to index type 'DataDict'.
Property 'title' does not exist on type 'DataDict'.
| キーの型 | 読める | 読めない |
|---|---|---|
| number | nd[0]、数字の文字列 nd["1"] |
nd["one"](TS7015) |
| symbol | sd[tag] |
sd["tag"](TS7053) |
`data-${string}` |
dd["data-id"] |
dd["title"](TS7053)。リテラルに title を書くと TS2353 |
パターンのキーは「data- で始まるキーだけを受け付ける辞書」になり、string の索引より範囲を絞れます。
keyof の結果もキーの型によって変わります。
| 型 | keyof の結果 |
|---|---|
{ [key: string]: number } |
string | number |
{ [index: number]: string } |
number |
{ [key: symbol]: boolean } |
symbol |
{ [key: `data-${string}`]: string } |
`data-${string}` |
string の索引で keyof が string | number になるのは、数値で引いても文字列に変換されて同じキーを指すためです。
interface と type で違う点:暗黙の索引シグネチャ(TS2345)
索引シグネチャを受け取る関数に、キーの決まったオブジェクトを渡す場面です。type で作った型は渡せますが、interface で作った型は渡せません。
// implicit.ts
function printAll(data: { [key: string]: string }) {
for (const [k, v] of Object.entries(data)) console.log(k, v);
}
type UserT = { name: string; mail: string };
interface UserI { name: string; mail: string }
const t: UserT = { name: "sato", mail: "sato@example.com" };
const i: UserI = { name: "sato", mail: "sato@example.com" };
printAll(t); // type で作った型:通る
printAll(i); // interface で作った型:TS2345
printAll({ ...i }); // スプレッドで写すと通る
interface UserI2 { name: string; mail: string; [key: string]: string }
const i2: UserI2 = { name: "sato", mail: "sato@example.com" };
printAll(i2); // 索引シグネチャを明示した interface:通る
type WithAge = { name: string; age: number };
const w: WithAge = { name: "sato", age: 30 };
printAll(w); // 値の型が合わない:TS2345
implicit.ts(12,10): error TS2345: Argument of type 'UserI' is not assignable to parameter of type '{ [key: string]: string; }'.
Index signature for type 'string' is missing in type 'UserI'.
implicit.ts(21,10): error TS2345: Argument of type 'WithAge' is not assignable to parameter of type '{ [key: string]: string; }'.
Property 'age' is incompatible with index signature.
Type 'number' is not assignable to type 'string'.
| 渡したもの | 結果 | エラーの2行目 |
|---|---|---|
| type で作った型(全プロパティが string) | 通る | なし |
| interface で作った型 | TS2345 | Index signature for type 'string' is missing |
| interface の値をスプレッドで写したもの | 通る | なし |
| 索引シグネチャを明示した interface | 通る | なし |
| type で作った型(number のプロパティあり) | TS2345 | Property 'age' is incompatible with index signature |
type で作ったオブジェクト型は、プロパティがすべて索引の値の型に合っていれば、索引シグネチャを持つものとして扱われます(暗黙の索引シグネチャ)。 interface にはこの扱いがありません。interface は後から同じ名前で宣言を足してプロパティを増やせるため、「今あるプロパティがすべて」とは言い切れないことが背景にあります。
直し方
| 直し方 | 書き方 |
|---|---|
| 渡す側を type にする | type UserT = { name: string; mail: string } |
| 渡すときに写す | printAll({ ...i }) |
| interface に索引を明示する | [key: string]: string を足す |
| 受け取る側をジェネリクスにする | function printAll<T extends Record<keyof T, string>>(data: T) |
4つ目は受け取る関数を変えられるときの方法で、interface の値をそのまま渡せます。
コピペ用:状態ごとの件数と担当者ごとの一覧を作る
2つのオプションを有効にした状態で動く、集計の完成形です。上の tsconfig.json と同じフォルダに置いて実行できます。
// tally.mts
// タスクを状態ごとに数え、担当者ごとにまとめる
type Task = { title: string; status: string; owner: string };
type Counter = { [status: string]: number };
type Groups = { [owner: string]: Task[] };
const tasks: Task[] = [
{ title: "見積もりを作る", status: "done", owner: "sato" },
{ title: "資料を直す", status: "doing", owner: "suzuki" },
{ title: "備品を数える", status: "todo", owner: "sato" },
{ title: "日程を決める", status: "done", owner: "suzuki" },
{ title: "議事録を書く", status: "done", owner: "sato" },
];
function countByStatus(list: Task[]): Counter {
const counter: Counter = {};
for (const t of list) {
counter[t.status] = (counter[t.status] ?? 0) + 1;
}
return counter;
}
function groupByOwner(list: Task[]): Groups {
const groups: Groups = {};
for (const t of list) {
(groups[t.owner] ??= []).push(t);
}
return groups;
}
const counter = countByStatus(tasks);
console.log(counter);
console.log("done:", counter["done"] ?? 0);
console.log("hold:", counter["hold"] ?? 0);
const groups = groupByOwner(tasks);
for (const [owner, list] of Object.entries(groups)) {
console.log(owner, list.map((t) => t.title).join(" / "));
}
$ npx tsc -p .
$ node tally.mts
{ done: 3, doing: 1, todo: 1 }
done: 3
hold: 0
sato 見積もりを作る / 備品を数える / 議事録を書く
suzuki 資料を直す / 日程を決める
Node.js 22.22.2 は、型注釈だけの TypeScript ファイルをそのまま実行できます。
集計の定番は (counter[key] ?? 0) + 1 と (groups[key] ??= []).push(...) の2つ です。どちらも「まだキーがない」場合を式の中で扱うので、オプションを有効にしていてもエラーになりません。
?? を省いた書き方と、ドットでの読み取りを同じ設定で確かめると、2つのオプションがそれぞれ知らせてくれます。
type Counter = { [status: string]: number };
const counter: Counter = { done: 3 };
counter["doing"] = counter["doing"] + 1;
console.log(counter.done);
ng.mts(3,20): error TS2532: Object is possibly 'undefined'.
ng.mts(4,21): error TS4111: Property 'done' comes from an index signature, so it must be accessed with ['done'].
エラー番号から直し方を引く
| 番号 | メッセージ(要約) | 原因 | 直し方 |
|---|---|---|---|
| TS1337 | cannot be a literal type or generic type | キーにリテラル・型引数・enum を書いた | { [K in Keys]: V } か Record<Keys, V> にする |
| TS1268 | must be 'string', 'number', 'symbol', or a template literal type | キーに boolean などを書いた | 書ける4種類のどれかにする |
| TS1019 | cannot have a question mark | キーに ? を付けた | ? を外す(索引はもともと任意のキー) |
| TS2411 | Property ... is not assignable to 'string' index type | 固定プロパティの型が索引の値の型に収まらない | 索引の値の型を広げる、または自由なキーを別プロパティに分ける |
| TS2413 | 'number' index type ... is not assignable to 'string' index type | number やパターンの索引が string の索引より広い | string 側を広げる |
| TS2374 | Duplicate index signature | 同じキーの型の索引を2つ書いた | 1つにまとめる |
| TS18048 / TS2532 | possibly 'undefined' | noUncheckedIndexedAccess が有効 | ??、undefined の確認、Object.entries |
| TS4111 | comes from an index signature | noPropertyAccessFromIndexSignature が有効 | obj["key"] と書く |
| TS2542 | only permits reading | readonly な索引へ書き込んだ | 新しいオブジェクトを作る |
| TS2345 | Index signature for type 'string' is missing | interface の値を索引シグネチャの引数へ渡した | type にする、スプレッドで写す、索引を明示する |
| TS7015 / TS7053 | implicitly has an 'any' type | 索引のキーの型に合わないキーで読んだ | キーの型に合う値で読む |
よくある質問
Q. [key: string] の key は別の名前でもよいですか?
A. はい。説明用の名前で、型の動きには影響しません。[itemName: string] や [status: string] のように、何のキーかが伝わる名前にすると読みやすくなります。名前を変えても同じキーの型の索引は1つしか書けません(TS2374)。
Q. Record と索引シグネチャ、どちらで書くとよいですか?
A. Record<string, V> と { [key: string]: V } は同じ型なので、キーが自由な辞書ならどちらでも構いません。キーがリテラルのユニオンなら Record(索引シグネチャでは TS1337)、固定プロパティと同じ場所に書きたい・キーに名前を付けたいなら索引シグネチャを選びます。
Q. noUncheckedIndexedAccess は strict に含まれますか?
A. TypeScript 7.0.2 で --strict だけを付けて確かめたところ、有効になりませんでした。tsconfig.json に "noUncheckedIndexedAccess": true を個別に書きます。noPropertyAccessFromIndexSignature も同じです。
Q. 配列の list[0] にも影響しますか?
A. noUncheckedIndexedAccess を有効にすると、string[] の list[0] は string | undefined になります。長さの決まったタプルの tuple[0] は変わりません。
まとめ
- インデックスシグネチャは
{ [key: string]: V }と書き、キーの名前を決めずに値の型だけを決める。Record<string, V>と同じ型 - キーに書けるのは string・number・symbol・テンプレートリテラル型(とそのユニオン)。 リテラルのユニオンは TS1337 になるので、Mapped Types か Record にする
- 固定プロパティの型は、索引の値の型に収まっている必要がある(TS2411)。 省略可能なプロパティとメソッドも対象
- 既定では、存在しないキーを読んでも undefined 込みの型にならない。 noUncheckedIndexedAccess を有効にすると、コンパイル時に知らせてくれる
- noPropertyAccessFromIndexSignature を有効にすると、索引由来のキーはブラケットで書く決まりになる(TS4111)
- readonly を付けると書き込みと delete が止まる(TS2542)。 効くのは1段目だけ
- type で作った型は索引シグネチャの引数へ渡せるが、interface で作った型は TS2345 になる。 スプレッドで写すか、索引を明示する
参考
- TypeScript Handbook「Object Types」(Index Signatures): https://www.typescriptlang.org/docs/handbook/2/objects.html
- TSConfig リファレンス noUncheckedIndexedAccess: https://www.typescriptlang.org/tsconfig/noUncheckedIndexedAccess.html
- TSConfig リファレンス noPropertyAccessFromIndexSignature: https://www.typescriptlang.org/tsconfig/noPropertyAccessFromIndexSignature.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript インデックスシグネチャの使い方|Recordとの違い」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。