- TypeScript
TypeScriptオブジェクト型定義|Recordとインデックスシグネチャ
TypeScriptのオブジェクト型定義とは、「どんなキーに、どんな型の値が入るか」をコンパイラに伝える書き方で、キーが決まっているなら type か interface、キーの一覧をユニオン型で持っているなら Record、キーが実行時に増えるならインデックスシグネチャか Map を選びます。 どれも「キーと値の組」を表せますが、キーの書き漏れを見つけてくれるか、存在しないキーを読んだときにどんな型になるかがそれぞれ違います。
この記事では、5つの書き方を 「キーは固定か可変か」「キーの網羅チェックがあるか」「存在しないキーを読んだときの型」 の3点で比べる選択表を最初に置きました。そのうえで、Record のキー不足エラーの再現、Partial や satisfies との書き分け、noUncheckedIndexedAccess をオンにしたときとオフのときの差を、TypeScript 7.0.2 と Node.js 22.22.2 で実際に動かした結果と一緒に紹介します。エラー文はすべて手元の tsc が出したものをそのまま載せています(ファイル名と行番号の表示だけ省いています)。
オブジェクト型定義の早見表
まずは選択表です。迷ったらこの表の「向いている場面」から逆引きしてください。
| 書き方 | キー | キーの網羅チェック | 存在しないキーを読むと | 向いている場面 |
|---|---|---|---|---|
type / interface で列挙 |
固定(名前ごとに値の型が違ってよい) | あり(不足は TS2741) | コンパイルエラー(TS2339) | 会員・記事・注文など、形が決まったデータ |
Record<ユニオン, V> |
固定(ユニオン型の全メンバー) | あり(不足は TS2741、余分は TS2353) | コンパイルエラー | 状態ごとのラベル、種別ごとの設定など「全部そろえたい」対応表 |
Partial<Record<ユニオン, V>> |
固定(ただし省略できる) | 余分なキーだけ検出 | V | undefined |
一部のキーだけ上書きする設定 |
Record<string, V> / インデックスシグネチャ |
可変 | なし | 既定では V、noUncheckedIndexedAccess がオンなら V | undefined |
ID をキーにした辞書、集計結果 |
Map<K, V> |
可変(文字列以外もキーにできる) | なし | get は常に V | undefined |
追加と削除が多い辞書、件数や順序が必要な辞書 |
よく使う書き方をコードでも並べておきます。
// 1. キーが決まっている:type(interface でも同じ形で書けます)
type User = { id: number; name: string };
// 2. キーの一覧がユニオン型:Record
type Status = "todo" | "doing" | "done";
const labels: Record<Status, string> = { todo: "未着手", doing: "作業中", done: "完了" };
// 3. 一部だけ持てばよい:Partial<Record>
const colors: Partial<Record<Status, string>> = { done: "green" };
// 4. キーが実行時に増える:Record<string, V> またはインデックスシグネチャ
const stock: Record<string, number> = { pen: 3 };
type Stock = { [itemId: string]: number }; // 上と同じ意味
// 5. 追加・削除が多い辞書:Map
const stockMap = new Map<string, number>([["pen", 3]]);
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScriptオブジェクト型定義|Recordとインデックスシグネチャ」
5つの書き方を選ぶ流れ
選び方の中心にあるのは 「キーの名前をコードを書く時点で知っているか」 です。知っているなら、コンパイラにキーの書き漏れや打ち間違いを調べてもらえます。知らない(実行時に決まる)なら、「そのキーは無いかもしれない」という前提で値を読む形にします。
flowchart TD
A[キーの名前は書く時点で決まっている?] -->|はい| B[キーごとに値の型が違う?]
A -->|いいえ 実行時に決まる| E[追加と削除が多い または 文字列以外をキーにしたい?]
B -->|はい| C[type か interface で列挙]
B -->|いいえ 値の型は共通| D[すべてのキーが必須?]
D -->|はい| F["Record<ユニオン, V>"]
D -->|いいえ| G["Partial<Record<ユニオン, V>>"]
E -->|はい| H["Map<K, V>"]
E -->|いいえ| I["Record<string, V> と noUncheckedIndexedAccess"]
以降のコードは、次の環境とコマンドで確認しました。
| 項目 | 内容 |
|---|---|
| TypeScript | 7.0.2(npm で作業フォルダにインストール) |
| Node.js | 22.22.2(.ts ファイルをそのまま実行) |
| 型チェックのコマンド | npx tsc --noEmit --strict --target es2022 --module nodenext ファイル名.ts |
| noUncheckedIndexedAccess | 既定はオフ。オンのときはコマンドに --noUncheckedIndexedAccess を追加 |
キーが決まっているオブジェクトを type と interface で定義する
会員や記事のように、プロパティの名前と型が決まっているデータは、名前を1つずつ書くのが基本です。
type User = { id: number; name: string };
const u: User = { id: 1, name: "sato" };
u.email; // 定義にないプロパティを読む
const key: string = "name";
u[key]; // string 型の変数でキーを指定する
error TS2339: Property 'email' does not exist on type 'User'.
error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.
No index signature with a parameter of type 'string' was found on type 'User'.
定義にないプロパティを読むと TS2339、string 型の変数でキーを指定すると TS7053 になります。2つ目のメッセージに「index signature が見つからない」と書かれているとおり、任意の文字列で読みたい場合に必要になるのが、後で紹介するインデックスシグネチャです。User のようにキーが決まっている型では、変数の型を keyof User("id" か "name" のどちらか、という型)にすると、そのまま u[key] と書けます。
type と interface のどちらで書くかは、この記事の主題から外れるので要点だけにします。オブジェクトの形を表すだけなら結果は同じで、ユニオン型や Record のような型の計算を伴うものは type でしか書けません。以降の例では type を使います。
Record でキーの書き漏れをコンパイル時に見つける
Record は Record<キーの型, 値の型> と書く組み込みのユーティリティ型です。キーにユニオン型を渡すと、「ユニオンの全メンバーをキーに持つオブジェクト」という型になり、1つでも欠けるとエラーになります。 これが Record を使ういちばんの理由です。
キーが足りないとき(TS2741)
type Status = "todo" | "doing" | "done";
const labels: Record<Status, string> = {
todo: "未着手",
doing: "作業中",
};
error TS2741: Property 'done' is missing in type '{ todo: string; doing: string; }' but required in type 'Record<Status, string>'.
メッセージには 足りないキーの名前(done) がそのまま出ます。後から Status に "archived" を足したときも、この対応表を直し忘れていれば同じエラーで知らせてくれるので、状態や種別が増えていくコードで特に役立ちます。
余分なキーを書いたとき(TS2353)
const labels2: Record<Status, string> = {
todo: "未着手",
doing: "作業中",
done: "完了",
archived: "保管",
};
error TS2353: Object literal may only specify known properties, and 'archived' does not exist in type 'Record<Status, string>'.
ユニオンにないキーも検出されます。キー名の打ち間違い(dona など)もこのエラーで見つかります。
keyof と組み合わせて「同じキーで値だけ違う型」を作る
既存の型と同じキーを持たせたいときは、keyof でキーのユニオンを取り出して Record に渡します。
type P = { id: number; name: string };
type Flags = Record<keyof P, boolean>; // { id: boolean; name: boolean } と同じ
const f: Flags = { id: true };
error TS2741: Property 'name' is missing in type '{ id: true; }' but required in type 'Flags'.
入力フォームの「各項目を表示するかどうか」「各項目にエラーがあるかどうか」のように、元の型とキーをそろえたい対応表に向いています。
コピペ用:状態ごとのラベルと件数を集計する完成形
Record を2か所で使った、そのまま動く例です。キーの一覧を配列で持ち、そこからユニオン型を作っているので、配列に1つ足すだけで型も一緒に広がります。
// full.ts
const STATUSES = ["todo", "doing", "done"] as const;
type Status = (typeof STATUSES)[number]; // "todo" | "doing" | "done"
const labels: Record<Status, string> = {
todo: "未着手",
doing: "作業中",
done: "完了",
};
const counts: Record<Status, number> = { todo: 0, doing: 0, done: 0 };
const tasks: { title: string; status: Status }[] = [
{ title: "見積もり", status: "done" },
{ title: "設計", status: "doing" },
{ title: "レビュー", status: "todo" },
{ title: "テスト", status: "todo" },
];
for (const t of tasks) counts[t.status] += 1;
for (const s of STATUSES) {
console.log(`${labels[s]}: ${counts[s]}件`);
}
$ node full.ts
未着手: 2件
作業中: 1件
完了: 1件
counts[t.status] は、キーがユニオン型なので必ず number になり、undefined の確認なしで足し算ができます。この例は noUncheckedIndexedAccess をオンにした型チェックもエラーなしで通りました。
一部のキーだけ持たせたいときは Partial と組み合わせる
「全部のキーは要らない、指定したものだけ上書きしたい」というときは、Record を Partial で包みます。Partial はすべてのプロパティを省略可能にするユーティリティ型です。
const colors: Partial<Record<Status, string>> = { done: "green" }; // 1つだけでも通る
const c: string = colors.done;
error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
キー不足のエラーは出なくなり、代わりに読み出した値が string | undefined になります。 「無いかもしれない」が型に現れるので、読む側で既定値を決めます。
const color = colors.done ?? "gray"; // 無ければ gray
値をそのまま使おうとすると、次のように知らせてくれます。
const part: Partial<Record<Status, number>> = {};
part.todo.toFixed();
error TS18048: 'part.todo' is possibly 'undefined'.
なお、Partial にしてもユニオンにないキーは引き続き検出されます。「キーの候補は決まっているが、全部は要らない」 という場面にちょうど合う書き方です。
| やりたいこと | 書き方 | 読み出した値の型 |
|---|---|---|
| 全キー必須の対応表 | Record<Status, string> |
string |
| 一部だけの上書き設定 | Partial<Record<Status, string>> |
string | undefined |
| 書き換えできない対応表 | Readonly<Record<Status, string>> |
string(代入は TS2540) |
3行目の Readonly も確認しておきます。
const ro: Readonly<Record<Status, number>> = { todo: 1, doing: 2, done: 3 };
ro.todo = 5;
error TS2540: Cannot assign to 'todo' because it is a read-only property.
satisfies で「キーの網羅チェック」と「値の細かい型」を両立する
型注釈(const x: 型 = ...)は変数の型を注釈した型に決めますが、satisfies は 「この型に合っているかだけを調べ、変数の型は値から推論したまま残す」 演算子です。Record と一緒に使うと違いがはっきりします。
キー不足は satisfies でも検出される
const price2 = { todo: 0, doing: 10 } satisfies Record<Status, number>;
error TS2741: Property 'done' is missing in type '{ todo: number; doing: number; }' but required in type 'Record<Status, number>'.
網羅チェックは型注釈のときと同じように働きます。
キーが可変の Record では、読めるキーが変わる
違いが出るのは Record<string, V> のような広い型と組み合わせたときです。
// 型注釈:変数の型は Record<string, number> になる
const anno: Record<string, number> = { pen: 3 };
anno.pen.toFixed(0); // 通る
anno.eraser; // これも通る(どんなキーでも読める型のため)
// satisfies:変数の型は { pen: number } のまま
const sat = { pen: 3 } satisfies Record<string, number>;
sat.eraser;
error TS2339: Property 'eraser' does not exist on type '{ pen: number; }'.
型注釈では「どんな文字列キーでも読める」型になるため、書いていない eraser を読んでもエラーになりません。satisfies なら 「値はすべて number」という確認をしつつ、実際に書いたキーだけが読める型 が残ります。
| 書き方 | キー不足の検出 | 変数の型 | 向いている場面 |
|---|---|---|---|
const x: Record<K, V> = {...} |
あり | Record<K, V> |
後からキーを足す辞書、関数の引数や戻り値 |
const x = {...} satisfies Record<K, V> |
あり | 値から推論した型 | 書き換えない設定値や定数の対応表 |
設定値のように「定義した後は読むだけ」のオブジェクトには satisfies、後から stock[id] = 1 のようにキーを足していく辞書には型注釈、と使い分けると収まりがよくなります。
キーが実行時に決まるときはインデックスシグネチャ
商品IDや会員IDのように、キーの名前がコードを書く時点でわからないときは、インデックスシグネチャを使います。[名前: キーの型]: 値の型 という形で、「このキーの型なら何でも受け付ける」と宣言します。
type Stock = { [itemId: string]: number };
const stock: Stock = { pen: 3 };
stock["eraser"] = 10; // 後から自由に足せる
itemId の部分は説明用の名前で、何と書いても型の意味は変わりません。読む人にキーが何を表すかを伝えるラベルとして使えます。
Record<string, V> との違い
Record<string, number> と { [k: string]: number } は同じ意味の型です。書き方の面で次の違いがあります。
| 観点 | インデックスシグネチャ | Record |
|---|---|---|
| キーの説明を書けるか | 書ける([itemId: string]) |
書けない |
| ユニオン型をキーにできるか | できない(TS1337) | できる |
| 固定のプロパティと混ぜられるか | 混ぜられる | Record 単体では混ぜられない |
| 1行で短く書けるか | やや長い | 短い |
ユニオン型はインデックスシグネチャのキーに書けない(TS1337)
type K1 = { [k: Status]: number };
error TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
メッセージが案内しているマップ型(in を使う書き方)に直すと通ります。そして、この書き方こそが Record の中身です。
type K2 = { [k in Status]: number }; // Record<Status, number> と同じ
インデックスシグネチャのキーに書けるのは、string・number・symbol と、テンプレートリテラルで表したパターンです。パターンを使うと「data- で始まるキーだけ」といった制約を付けられます。
type K3 = { [k: `data-${string}`]: string };
const d: K3 = { "data-id": "1", other: "x" };
error TS2353: Object literal may only specify known properties, and 'other' does not exist in type 'K3'.
固定のプロパティと混ぜるときは値の型をそろえる(TS2411)
インデックスシグネチャは、固定のプロパティと同じ型の中に書けます。ただし、固定のプロパティも「文字列キーの1つ」なので、値の型がインデックスシグネチャの型に収まっている必要があります。
interface Bad { id: number; [k: string]: string }
error TS2411: Property 'id' of type 'number' is not assignable to 'string' index type 'string'.
値の型を string | number に広げるか、可変の部分を extras: Record<string, string> のように別のプロパティへ分けると通ります。後者のほうが、固定の部分と可変の部分の境目が読み取りやすくなります。
number をキーにしても実行時は文字列になる
const byNum: Record<number, string> = { 1: "a" };
console.log(Object.keys(byNum));
[ '1' ]
オブジェクトのキーは実行時には文字列として保存されます。数値のままキーにしたい場合は、後で紹介する Map が向いています。
存在しないキーを読んだときの型と noUncheckedIndexedAccess
ここがインデックスシグネチャと Record<string, V> でいちばんつまずきやすいところです。既定の設定では、どんなキーを読んでも値の型は V になり、「無いかもしれない」が型に現れません。
オフのとき:型チェックは通るが、実行すると止まる
// stock.ts
type Stock = Record<string, number>;
const stock: Stock = { pen: 3 };
function show(itemId: string) {
const count = stock[itemId];
console.log(itemId, count.toFixed(0));
}
show("pen");
show("eraser");
$ npx tsc --noEmit --strict --target es2022 --module nodenext stock.ts
(エラーなし)
$ node stock.ts
pen 3
/home/taro/app/stock.ts:6
console.log(itemId, count.toFixed(0));
^
TypeError: Cannot read properties of undefined (reading 'toFixed')
(パスの表示は環境に合わせて置き換えています。)strict をオンにしていても、count は number と推論されるため型チェックは通り、登録のない eraser を読んだところで実行時エラーになります。
オンのとき:コンパイル時に知らせてくれる
同じファイルに --noUncheckedIndexedAccess を付けて型チェックします。
$ npx tsc --noEmit --strict --noUncheckedIndexedAccess --target es2022 --module nodenext stock.ts
stock.ts(6,23): error TS18048: 'count' is possibly 'undefined'.
count の型が number | undefined に変わり、確認せずに使った箇所が TS18048 で示されます。 実行する前に気づけるようになりました。
直した後のコード
// stock_fixed.ts
type Stock = Record<string, number>;
const stock: Stock = { pen: 3 };
function show(itemId: string) {
const count = stock[itemId];
if (count === undefined) {
console.log(itemId, "登録なし");
return;
}
console.log(itemId, count.toFixed(0)); // ここでは number に絞り込まれている
}
show("pen");
show("eraser");
console.log(stock["eraser"] ?? 0);
$ node stock_fixed.ts
pen 3
eraser 登録なし
0
このファイルはオンの設定でもエラーなしで通りました。既定値を決めるだけでよい場面では ?? が短く書けます。
オンとオフで変わる型の一覧
同じ変数宣言から読み出した値の型を、tsc に型定義ファイル(.d.ts)を出力させて比べました。
| 読み出す式 | オフ(既定) | オン |
|---|---|---|
インデックスシグネチャ stock.eraser / stock["eraser"] |
number |
number | undefined |
Record<string, number> の rec.x |
number |
number | undefined |
Record<Status, number> の recU.todo |
number |
number(変わらない) |
Partial<Record<Status, number>> の part.todo |
number | undefined |
number | undefined |
Map<string, number> の map.get("x") |
number | undefined |
number | undefined |
配列 number[] の arr[0] |
number |
number | undefined |
タプル [number, number] の tup[0] |
number |
number(変わらない) |
読み取れることは3つあります。
- キーや長さが型でわかっているもの(ユニオンキーの Record、タプル)は、オンにしても変わりません。 余計な undefined 確認は増えません。
- Partial と Map は、設定に関係なく最初から undefined を含みます。
- 配列の添字アクセスにも効きます。 オンにすると
arr[0]の確認も求められる点は、切り替える前に知っておくと慌てずに済みます。
オンにしても確認なしで書けるパターン
オンの設定で、次の3つの書き方はエラーなしで通ることを確かめました。
declare const stock: { [itemId: string]: number };
// 1. Object.entries で回す(値は number のまま受け取れる)
for (const [k, val] of Object.entries(stock)) { val.toFixed(0); }
// 2. リテラル型の定数キーを in で確認してから読む
const id = "pen";
if (id in stock) { stock[id].toFixed(0); }
// 3. いったん変数に入れて undefined と比べる
const x = stock[id];
if (x !== undefined) x.toFixed(0);
2 は id が "pen" というリテラル型の定数だったときの結果です。キーが string 型の変数のときは、3 の「変数に入れてから比べる」形が確実です。
どこで設定するか
公式の TSConfig リファレンスでは、noUncheckedIndexedAccess の既定値は false で、strict をオンにしても一緒にはオンになりません。一方、手元の TypeScript 7.0.2 で npx tsc --init を実行して作られた tsconfig.json には、次の行が最初から入っていました。
{
"compilerOptions": {
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true
}
}
(生成されたファイルから該当の3行を抜き出しています。)つまり、新しく tsc --init で始めた環境ではオン、以前から使っている tsconfig.json に書いていなければオフ、という状態になりやすいということです。記事やサンプルのコードを自分の環境に貼ったときに挙動が違ったら、まずこの設定を確かめてください。
Map を選ぶ場面と、オブジェクトとの比べ方
Map は JavaScript に組み込まれた辞書のクラスです。型は Map<キーの型, 値の型> と書きます。
const map = new Map<string, number>([["pen", 3]]);
const v = map.get("eraser");
v.toFixed(0);
error TS18048: 'v' is possibly 'undefined'.
get の戻り値は、設定に関係なく常に V | undefined です。「無いかもしれない」を必ず確認する形になるので、noUncheckedIndexedAccess をオフにしている環境でも同じ安全さが得られます。キーの型にユニオンを指定すれば、set に渡すキーも調べてくれます。
const mk = new Map<Status, number>();
mk.set("other", 1);
error TS2345: Argument of type '"other"' is not assignable to parameter of type 'Status'.
ただし、Map には「全部のキーがそろっているか」を調べる仕組みはありません。網羅チェックが欲しいときは Record が向いています。
オブジェクトは継承したプロパティも見つかる
オブジェクトを辞書として使うときに知っておきたい実行時の性質があります。
const m = new Map<string, number>();
m.set("pen", 3);
console.log(m.get("pen"), m.get("eraser"), m.has("eraser"), m.size);
const o: Record<string, number> = {};
console.log("toString" in o, typeof o["toString"], new Map().has("toString"));
3 undefined false 1
true function false
空のオブジェクトでも、"toString" in o は true になり、o["toString"] を読むと関数が返ります。型の上では number のはずの場所に関数が入っている、ということです。利用者が入力した文字列をそのままキーにする辞書では、Map を使うとこの取り違えが起きません。
Record(オブジェクト)と Map の比較
| 観点 | Record<string, V> |
Map<K, V> |
|---|---|---|
| キーにできる型 | string・number・symbol(実行時は文字列かシンボル) | 何でも(数値やオブジェクトもそのまま) |
| 存在しないキーの型 | 設定しだいで V か V | undefined |
常に V | undefined |
| 件数の取得 | Object.keys(o).length |
map.size |
| 存在確認 | key in o(継承したプロパティも true) |
map.has(key) |
| JSON との変換 | JSON.stringify でそのまま変換できる |
配列などへの変換が必要 |
| リテラルでの初期化 | { pen: 3 } と短く書ける |
new Map([["pen", 3]]) |
| キーの網羅チェック | ユニオンキーの Record ならあり | なし |
API から受け取った JSON や設定ファイルのように 「データとして受け渡す」ものは Record、プログラムの中で追加と削除を繰り返す作業用の辞書は Map と考えると選びやすくなります。
Object.keys で回すとキーが string になる
Record を使い始めると、多くの人が一度は出会うエラーです。
declare const recU: Record<Status, number>;
for (const k of Object.keys(recU)) {
const val = recU[k];
}
error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<Status, number>'.
No index signature with a parameter of type 'string' was found on type 'Record<Status, number>'.
Object.keys の戻り値は string[] で、Status の配列にはなりません。TypeScript の型は「少なくともこのプロパティを持つ」という意味なので、実行時にはほかのキーが付いている可能性があり、コンパイラは控えめに string としています。対応は2つあります。
| 方法 | コード | 特徴 |
|---|---|---|
| キーの配列を先に定義して回す | for (const s of STATUSES) { labels[s] } |
型アサーションなしで書ける。順序も自分で決められる |
| 型アサーションで伝える | Object.keys(recU) as Status[] |
短い。余分なキーが無いことは書き手が受け持つ |
おすすめは1つ目です。前半の完成形コードのように as const の配列からユニオン型を作っておくと、配列が「実行時のキー一覧」と「型のキー一覧」を兼ねるので、両者がずれません。
つまずきやすいエラーの一覧
この記事で再現したエラーを、原因と直し方つきでまとめます。
| エラーコード | 出る場面 | 直し方 |
|---|---|---|
| TS2741 | Record や type で必須のキーが足りない | 足りないキーを書く。全部は要らないなら Partial で包む |
| TS2353 | ユニオンや定義にないキーを書いた | キー名を直す。新しいキーならユニオン型に追加する |
| TS2339 | 定義にないプロパティを読んだ | 型にプロパティを足す。satisfies のときは型注釈との違いを確認する |
| TS7053 | string 型の変数でキーを指定した | 変数の型を keyof 型 やユニオン型にする。可変ならインデックスシグネチャにする |
| TS18048 | undefined かもしれない値をそのまま使った | === undefined で確認する、または ?? で既定値を決める |
| TS2322 | V | undefined を V の変数に入れた |
既定値を決めるか、変数の型に undefined を含める |
| TS1337 | インデックスシグネチャのキーにユニオン型を書いた | [k in ユニオン] のマップ型か Record にする |
| TS2411 | 固定プロパティの型がインデックスシグネチャと合わない | 値の型を広げるか、可変の部分を別のプロパティに分ける |
| TS2540 | Readonly のプロパティに代入した | 新しいオブジェクトを作る。書き換えるなら Readonly を外す |
| TS2345 | Map の set にキーの型と合わない値を渡した | キーの値を直す。新しいキーならユニオン型に追加する |
書く前のチェックリスト
- [ ] キーの名前を書く時点で知っているかを最初に決めた(知っている → type・interface・Record、知らない → インデックスシグネチャ・Map)
- [ ] 状態や種別の対応表は
Record<ユニオン, V>にして、キーの書き漏れをコンパイラに任せた - [ ] 「一部だけ」でよい設定は
Partial<Record<ユニオン, V>>にして、読む側で既定値を決めた - [ ] 読むだけの定数は satisfies、後からキーを足す辞書は型注釈で書いた
- [ ]
Record<string, V>を使う箇所で、noUncheckedIndexedAccess がオンかオフかを tsconfig.json で確かめた - [ ] 入力された文字列をキーにする辞書は Map を検討した
- [ ] ループは
Object.keysではなく、キーの配列(as const)から回す形を検討した
よくある質問
Q. Record<string, V> とインデックスシグネチャは、どちらで書けばよいですか? A. 型としては同じ意味なので、読みやすいほうで構いません。キーが何を表すか(itemId など)を型の中に書いておきたいとき、または固定のプロパティと同じ型にまとめたいときはインデックスシグネチャ、短く書きたいときや後でユニオンキーに切り替える可能性があるときは Record が合います。
Q. noUncheckedIndexedAccess は途中からオンにしても大丈夫ですか?
A. オンにすると、インデックスシグネチャと配列の添字アクセスの型に undefined が加わるため、確認なしで使っている箇所に TS18048 などが出ます。ユニオンキーの Record やタプルは変わらないことを本文の一覧で確かめました。エラーの数を先に見たいときは、tsconfig.json を変える前にコマンドへ --noUncheckedIndexedAccess を付けて型チェックだけ走らせると、影響の大きさがわかります。
Q. Record のキーを後から増やしたいときはどうしますか?
A. キーの一覧を as const の配列で持ち、そこから (typeof 配列)[number] でユニオン型を作っておくと、配列に1つ足すだけで済みます。Record で書いた対応表は、足りないキーが TS2741 で示されるので、エラーを順に直していけば直し漏れがなくなります。
Q. satisfies を付けたら Object.keys のキーも絞られますか?
A. 絞られません。Object.keys の戻り値は、オブジェクトの型に関係なく string[] です。キーごとに処理したいときは、キーの配列を先に定義して回す形が素直です。
まとめ
- キーが決まっていてキーごとに値の型が違うなら type か interface。定義にないプロパティは TS2339 で見つかる
- キーの一覧がユニオン型なら
Record<ユニオン, V>。不足は TS2741、余分は TS2353 で、足りないキー名までメッセージに出る - 一部だけでよいなら
Partial<Record<ユニオン, V>>。読み出した値がV | undefinedになるので既定値を決める - 読むだけの定数は satisfies。網羅チェックを受けつつ、実際に書いたキーだけが読める型が残る
- キーが実行時に決まるなら
Record<string, V>かインデックスシグネチャ。存在しないキーの型は noUncheckedIndexedAccess がオフだとV、オンだとV | undefined - Map の get は設定に関係なく
V | undefined。継承したプロパティとの取り違えもなく、追加と削除が多い辞書に向く - ループはキーの配列(
as const)から回すと、型アサーションなしで Record を読める
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScriptオブジェクト型定義|Recordとインデックスシグネチャ」
株式会社RJCでは、TypeScriptのオブジェクト型定義やRecordの使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。