TypeScriptでオブジェクトに型をつけるとき、とりあえず object と書いてみたものの、いざプロパティへアクセスしたらエラーになって戸惑った経験はありませんか。

オブジェクトの型って object と書けばいいんじゃないの?なんでプロパティが読めないの…?
object と Object と {} 、見た目が似てるけど何が違うんだ?どれを使えばいいのか分からない。
キー名が事前に決まらないオブジェクトって、どう型をつければいいんだろう?

オブジェクトの型付けは毎日のように書く割に、object という型は実はほとんど役に立たず、紛らわしい3つの型の違いや動的キーの扱いでつまずきやすいところです。この記事では型注釈の基本から、紛らわしい型の違いを1枚の比較表で、動的キーや修飾子の正しい書き方、そして型定義手段の選び分けまでを判断フロー付きで身につけて、迷わずオブジェクトに型を書けるようになります。

この記事は次のような方におすすめです。

この記事はこんな人におすすめ!
  • オブジェクトの型注釈をどう書くか毎回迷っている方
  • object型・Object型・{}型の違いがあいまいな方
  • キーが動的なオブジェクトを型安全に書きたい方
  • readonlyやオプショナルなど修飾子の付け方を整理したい方

読み終えるころには、オブジェクトのどんな形にも適切な型を当てはめられ、object{} に頼らず安全なコードを書けるようになります。

それでは、順を追って詳しく見ていきましょう!

オブジェクト型の型注釈の基本

TypeScriptでオブジェクトの型を書く基本は、プロパティ名とその型を { name: string; age: number } のように列挙することです。object のような大きな型でひとくくりにするのではなく、どんなプロパティがどんな型で入るかを一つずつ書きます。

let user: { name: string; age: number };

user = { name: "Taro", age: 30 }; // OK

このようにプロパティを列挙すれば、user.namestringuser.agenumber として安全に扱えます。関数の引数にもそのまま書けます。

function greet(person: { name: string; age: number }) {
  return `${person.name}さん(${person.age}歳)`;
}

オブジェクトが入れ子になる場合は、プロパティの型としてオブジェクト型をそのまま書きます。

let profile: {
  name: string;
  address: { city: string; zip: string };
};

profile = { name: "Taro", address: { city: "Tokyo", zip: "100-0001" } };

型に定義したプロパティが足りない場合はエラーになります。また、オブジェクトリテラルを直接代入すると、余分なプロパティも余剰プロパティチェックでエラーになります。次のように必要なプロパティを欠くと、不足を指摘されます。

let user: { name: string; age: number };

user = { name: "Taro" }; // 例:TS2741 Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'.

プロパティの型が合わなければ代入できず、型注釈どおりの形だけが通ります。これがオブジェクトに型をつける土台で、ここから先の修飾子や動的キーもすべてこの「プロパティを列挙する」書き方の上に積み上がります。

型エイリアスとinterfaceでの定義(概要)

同じオブジェクト型を何度も使い回したいときは、typeinterface で名前を付けて定義します。どちらでも同じ形のオブジェクト型を表現できます。

// 型エイリアス
type User = { name: string; age: number };

// インターフェース
interface UserI {
  name: string;
  age: number;
}

const a: User = { name: "Taro", age: 30 };
const b: UserI = { name: "Hanako", age: 28 };

typeinterface のどちらを選んでも、オブジェクト型を表す用途ではほぼ同じように書けます。ただし、それぞれには細かな違いや、用途に応じた選び分けの基準もあります。

余剰プロパティチェックでつまずく場面

型に定義していないプロパティを書くとエラーになるのは、オブジェクトリテラルを直接代入したときに働く余剰プロパティチェック(excess property checks)のためです。

type User = { name: string; age: number };

const u: User = { name: "Taro", age: 30, role: "admin" };
// 例:TS2353 Object literal may only specify known properties, and 'role' does not exist in type 'User'.(TypeScriptバージョンにより文言が異なる場合あり)

このチェックはタイプミス(naem のような書き間違い)を早期に検出するためのもので、リテラルを直接渡すときだけ厳しく働きます。一方、いったん変数に入れてから代入すると、このチェックは通ります

type User = { name: string; age: number };

const tmp = { name: "Taro", age: 30, role: "admin" };
const u: User = tmp; // OK(余剰プロパティチェックが働かない)

変数経由では「User が要求するプロパティを満たしているか」だけが見られるため、余分な role があっても問題になりません。意図的に追加プロパティを許したい場合は、後述するインデックスシグネチャを足すか、型自体に該当プロパティを定義して受け入れます。

object型・Object型・{}型の違い

結論から言うと、objectObject{} はいずれも「広すぎて使いどころが乏しい型」で、実務では具体的なオブジェクト型を書くのが基本です。3つはそれぞれ受け入れる値の範囲が異なります。

  • 小文字の object:プリミティブ(string・number・boolean・null・undefined など)以外の値、つまり配列・関数・オブジェクトを受ける
  • 大文字の Object:JavaScriptの Object インターフェースに対応し、nullundefined 以外のほぼすべての値を受ける
  • {}:プロパティを1つも持たない型に見えて、実際は nullundefined 以外のほぼすべての値を受ける

代入できる値の範囲を1枚にまとめると、違いがはっきりします。

代入する値 object Object {} { name: string }(具体型)
42(number) ⭕️ ⭕️
"text"(string) ⭕️ ⭕️
true(boolean) ⭕️ ⭕️
null / undefined
{ name: "Taro" } ⭕️ ⭕️ ⭕️ ⭕️
[1, 2, 3](配列) ⭕️ ⭕️ ⭕️

nullundefined の可否は strictNullChecks が有効な場合です。無効な場合はこれらも代入できてしまいます。

最大の落とし穴は、object 型に入れた値はプロパティへアクセスできないことです。

const o: object = { a: 1 };
o.a; // 例:TS2339 Property 'a' does not exist on type 'object'.(TypeScriptバージョンにより文言が異なる場合あり)

object は「プリミティブではない何か」としか言っておらず、どんなプロパティを持つかの情報がないため、o.a を読もうとすると弾かれます。これを避けるには、{ a: number } のように具体的なプロパティを書いた型注釈にするだけで解決します。Object{} はプリミティブまで受け入れてしまい型チェックがほとんど効かないので、これらも積極的に使う理由はありません。なお Object が持つメソッド(toString など)はJavaScript実行時の挙動の話で、その詳細はMDNの MDN Web Docs「Object」 で確認できます。

なぜ {} 型は危険なのか

{} は名前の見た目から「空オブジェクトの型」と誤解されがちですが、実際には nullundefined 以外のほぼすべての値を受け入れる、かなり広い型です。

let x: {};

x = { a: 1 }; // OK
x = 42;       // OK(numberも通ってしまう)
x = "hello";  // OK(stringも通ってしまう)
x = true;     // OK

このように numberstring まで素通りしてしまうため、{} で型を絞り込んだつもりでも実質的に何のチェックにもなりません。「何が来るか分からない値」を一度受けて安全に絞り込みたいなら、{} ではなく unknown が適切です。unknown は何でも受けますが、使う前に型を確認するよう強制してくれるため安全に扱えます。

プロパティの修飾:オプショナルとreadonly

省略してもよいプロパティは ? を付けたオプショナルプロパティに、後から変更させたくないプロパティは readonly 修飾子で表します。

type User = {
  name: string;
  age?: number;        // オプショナル:あってもなくてもよい
  readonly id: number; // readonly:初期化後は再代入不可
};

const u: User = { name: "Taro", id: 1 }; // ageは省略してOK

オプショナルにしたプロパティの型は、未指定の場合を含めて number | undefined として扱われるため、使う前に存在チェックが必要になります。一方 readonly を付けたプロパティに再代入しようとすると型エラーになります。

const u: User = { name: "Taro", id: 1 };

u.id = 2; // 例:TS2540 Cannot assign to 'id' because it is a read-only property.(TypeScriptバージョンにより文言が異なる場合あり)

readonly はあくまで型レベルでの再代入防止で、実行時にオブジェクトを本当に凍結するわけではない点に注意してください。配列にも readonly string[] のように付けられ、要素の追加・変更を型レベルで防げます。

なかむぅ
なかむぅ
オブジェクト全体を読み取り専用にする Readonly<T> など、readonly をさらに使いこなす方法はこちらの記事が参考になります。
TypeScriptのreadonlyを完全理解|効かない罠と3つの書き分けTypeScriptのreadonlyを付けたはずなのに、いつの間にか値が書き換わっていた——そんな経験はありませんか? ...

なお age?.toString() のようなオプショナルチェーン演算子は、こうした省略可能なプロパティを安全にたどるときに使います。

動的なキーを持つオブジェクト:インデックスシグネチャ

キー名が事前に決まらないオブジェクトには、インデックスシグネチャ [key: string]: number を使います。たとえば次のコードのように「どんな文字列キーでも値は number」と宣言します。

type Scores = { [key: string]: number };

const scores: Scores = {};
scores["math"] = 80;
scores["english"] = 90; // どんなキーでも追加できる

逆に、インデックスシグネチャを定義していない素のオブジェクト型に対して文字列変数でキー指定すると、「型 string の式を使ってインデックスシグネチャを持たない型を参照できない」旨のエラー(TS7053系。文言はバージョンで異なります)になります。動的な文字列キーでアクセスしたいときは、上のように [key: string]: 値の型 を型へ加えるのが解決策です。

インデックスシグネチャのキーに使える型は stringnumbersymbol・テンプレートリテラル型、またはそれらだけからなるユニオン型に限られます。既知のプロパティと併用することもでき、その場合は固定プロパティの型がインデックスの値型と矛盾しないようにします。

type Config = {
  version: string; // 固定プロパティ version も string なので両立する
  [key: string]: string;
};

注意したいのは、存在しないキーにアクセスしても、既定では値の型(ここでは number)として扱われることです。

type Scores = { [key: string]: number };
const scores: Scores = { math: 80 };

const s = scores["science"]; // sの型は number(実行時の値は undefined なのに!)
s.toFixed(2); // 型エラーにならず素通りしてしまう

実行時には undefined が返るのに、型のうえでは number と見なされてしまうため、ここに潜在的なバグの温床があります。これを安全側に倒す設定が次のオプションです。

noUncheckedIndexedAccess で安全にする

noUncheckedIndexedAccess をオンにすると、インデックスシグネチャ経由のアクセス結果が自動で T | undefined 型になり、存在しないキーへのアクセスをコンパイラが警告してくれます。

// noUncheckedIndexedAccess: true の場合
type Scores = { [key: string]: number };
const scores: Scores = { math: 80 };

const s = scores["science"]; // sの型は number | undefined
s.toFixed(2); // 例:TS18048 's' is possibly 'undefined'.(TypeScriptバージョンにより文言が異なる場合あり)

if (s !== undefined) {
  s.toFixed(2); // OK:存在チェック後は number として扱える
}

オフのときは number、オンのときは number | undefined と推論される違いを実際に試すと、設定の効果が一目で分かります。動的キーのオブジェクトを扱うなら、存在しないキーの見落としを型で防げるこの設定をオンにしておくのがおすすめです。

用途別に型定義手段を選ぶ

キーや値の性質によって、適した型定義の手段は変わります。次の判断フローで選ぶと迷いません。

  • キーが固定で事前に分かる → 型注釈 / interface でプロパティを列挙する
  • キーは可変だが文字列で、値の型が均一 → インデックスシグネチャ、または Record で表す
  • キーの追加・削除が頻繁、または文字列以外のキーを使う → Map を使う

固定キーの場合はこれまで見てきたプロパティ列挙が最適です。文字列キーで値が均一なら、インデックスシグネチャと同等のことを Record でより簡潔に書けます。キーを頻繁に出し入れする、あるいは数値やオブジェクトをキーにしたい場合は Map が向きます。

なかむぅ
なかむぅ
均一な値を持つマップ型を簡潔に書けるRecord型については、こちらで解説しています。
TypeScriptのRecord型とインデックスシグネチャ(index signature)の違いTypeScriptのRecord型の使い方を、インデックスシグネチャ(index signature)との違いから整理。キー網羅の強制やundefinedの落とし穴、ユニオンキー・Partial併用まで実コードで解説し、型安全な設計に迷わなくなります。...

【付録】さらに学びを深めるためのリソース


さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。

おすすめの書籍

ゼロからわかる TypeScript入門


技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。

変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。

プロを目指す人のためのTypeScript入門


技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。

オンラインで参照できる公式ドキュメント

TypeScript公式ハンドブック


https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。

TypeScript Deep Dive


https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。

TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。


まとめ – オブジェクト型は具体的に書くのが基本

この記事の要点をまとめます。

  • 基本は object でひとくくりにせず、具体的なプロパティを列挙した型を書く
  • objectObject{} は受け入れる範囲が広すぎて型チェックが甘く、原則として避ける
  • 動的キーはインデックスシグネチャで定義し、noUncheckedIndexedAccess で存在しないキーを安全に扱う
  • 固定キーは型注釈・interface、均一な文字列キーは Record、頻繁な出し入れは Map と用途で選び分ける

オブジェクトの形に合わせて適切な型を選べるようになれば、object{} に頼らずとも、安全で読みやすいコードを書けるようになります。

なかむぅ
なかむぅ
均一な値を持つオブジェクトをRecord型で表す方法は、こちらが参考になります。
TypeScriptのRecord型とインデックスシグネチャ(index signature)の違いTypeScriptのRecord型の使い方を、インデックスシグネチャ(index signature)との違いから整理。キー網羅の強制やundefinedの落とし穴、ユニオンキー・Partial併用まで実コードで解説し、型安全な設計に迷わなくなります。...

※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。