はじめに

TypeScriptでアプリケーションを開発する際、オブジェクトは最も頻繁に扱うデータ構造です。ユーザー情報、API レスポンス、設定値など、複数のプロパティを持つデータを扱うとき、その構造を「型」として定義することで、開発中のミスを防ぎ、コードの可読性を高められます。

本記事では、TypeScriptのオブジェクト型リテラル、interfaceの定義と利用方法、オプショナルプロパティ(?)、読み取り専用プロパティ(readonly)、インデックスシグネチャについて詳しく解説します。

この記事を読み終えると、以下のことができるようになります。

  • オブジェクト型リテラルでインラインに型を定義できる
  • interfaceを使って再利用可能なオブジェクト型を定義できる
  • オプショナルプロパティで省略可能なプロパティを表現できる
  • readonly修飾子で変更不可のプロパティを定義できる
  • インデックスシグネチャで動的なキーを持つオブジェクトを型付けできる

実行環境・前提条件

前提知識

  • JavaScriptのオブジェクト操作(プロパティアクセス、分割代入など)の基本
  • TypeScriptの基本的な型注釈の書き方(基本型入門を参照)
  • 配列とタプルの型付け(配列・タプル入門を参照)

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • 型定義に合致しないプロパティの代入がコンパイルエラーになる
  • 必須プロパティが欠けているオブジェクトの作成がコンパイルエラーになる
  • readonlyプロパティへの再代入がコンパイルエラーになる

TypeScriptのオブジェクト型とは

オブジェクト型は、JavaScriptのオブジェクトに対して「どのようなプロパティを持ち、それぞれがどの型か」を定義する仕組みです。TypeScriptでオブジェクト型を定義する方法は主に3つあります。

graph TB
    A[TypeScriptのオブジェクト型] --> B[オブジェクト型リテラル]
    A --> C[interface]
    A --> D[type エイリアス]
    B --> E["{ name: string; age: number }"]
    C --> F["interface User { ... }"]
    D --> G["type User = { ... }"]

本記事では、オブジェクト型リテラルとinterfaceに焦点を当てて解説します。typeエイリアスとの違いについては、後続の記事で詳しく取り上げます。

オブジェクト型リテラル - インラインで型を定義する

オブジェクト型リテラルは、変数や関数のパラメータに対して直接型を記述する方法です。小規模な型定義や一度しか使わない型に適しています。

基本的な構文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// オブジェクト型リテラルによる変数の型注釈
const user: { name: string; age: number } = {
  name: "田中太郎",
  age: 30
};

// 関数パラメータでの使用
function greet(person: { name: string; age: number }): string {
  return `こんにちは、${person.name}さん(${person.age}歳)`;
}

console.log(greet(user)); // "こんにちは、田中太郎さん(30歳)"

オブジェクト型リテラルでは、プロパティ名とその型を:で区切り、各プロパティは;または,で区切ります。

型チェックの動作

TypeScriptはオブジェクトリテラルを渡す際、定義された型に対して厳密なチェック(Excess Property Checks)を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function printUser(user: { name: string; age: number }): void {
  console.log(`${user.name}: ${user.age}歳`);
}

// 正しい呼び出し
printUser({ name: "佐藤", age: 25 }); // OK

// エラー: 存在しないプロパティ
printUser({ name: "鈴木", age: 28, email: "suzuki@example.com" });
// Error: Object literal may only specify known properties,
// and 'email' does not exist in type '{ name: string; age: number }'.

ただし、変数を経由して渡す場合は、余剰プロパティのチェックが緩和されます。

1
2
const person = { name: "鈴木", age: 28, email: "suzuki@example.com" };
printUser(person); // OK - 必要なプロパティが揃っていれば通る

interfaceでオブジェクト型を定義する

interfaceは、オブジェクトの構造を名前付きで定義する方法です。再利用性が高く、大規模なアプリケーションで広く使われています。

interfaceの基本構文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// interfaceでユーザー型を定義
interface User {
  name: string;
  age: number;
  email: string;
}

// interfaceを使用した変数宣言
const user: User = {
  name: "山田花子",
  age: 28,
  email: "hanako@example.com"
};

// 関数の引数と戻り値にinterfaceを使用
function createUser(name: string, age: number, email: string): User {
  return { name, age, email };
}

const newUser = createUser("高橋", 35, "takahashi@example.com");
console.log(newUser.name); // "高橋"

interfaceの命名規則

TypeScriptでは、interfaceの名前にIプレフィックスを付ける慣習(例:IUser)はあまり推奨されていません。Microsoftの公式ガイドラインでも、プレフィックスなしの命名が推奨されています。

1
2
3
4
5
6
7
8
9
// 推奨: プレフィックスなし
interface User {
  name: string;
}

// 非推奨: Iプレフィックス
interface IUser {
  name: string;
}

interfaceの拡張(extends)

interfaceextendsキーワードを使って他のinterfaceを継承できます。これにより、既存の型を拡張した新しい型を効率的に定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 基本となるinterface
interface Person {
  name: string;
  age: number;
}

// Personを拡張したEmployee
interface Employee extends Person {
  employeeId: string;
  department: string;
}

// Employeeは Person のプロパティも含む
const employee: Employee = {
  name: "中村",
  age: 32,
  employeeId: "E001",
  department: "開発部"
};

複数のinterfaceを同時に継承することも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Contactable {
  email: string;
  phone: string;
}

interface Addressable {
  address: string;
  postalCode: string;
}

// 複数のinterfaceを継承
interface Customer extends Contactable, Addressable {
  customerId: string;
  name: string;
}

const customer: Customer = {
  customerId: "C001",
  name: "山本商事",
  email: "info@yamamoto.co.jp",
  phone: "03-1234-5678",
  address: "東京都渋谷区...",
  postalCode: "150-0001"
};

宣言のマージ(Declaration Merging)

interfaceの特徴的な機能として、同じ名前で複数回宣言するとプロパティがマージされます。これはライブラリの型定義を拡張する際に便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface Config {
  apiUrl: string;
}

interface Config {
  timeout: number;
}

// 両方のプロパティが必要
const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
};

この機能は、サードパーティライブラリの型を拡張する際に特に有用です。

オプショナルプロパティ - 省略可能なプロパティを定義する

プロパティ名の後ろに?を付けることで、そのプロパティを省略可能(オプショナル)として定義できます。

オプショナルプロパティの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
interface UserProfile {
  name: string;           // 必須
  age: number;            // 必須
  nickname?: string;      // オプショナル(省略可能)
  bio?: string;           // オプショナル(省略可能)
}

// nicknameとbioを省略してもOK
const user1: UserProfile = {
  name: "田中",
  age: 25
};

// すべて指定してもOK
const user2: UserProfile = {
  name: "佐藤",
  age: 30,
  nickname: "サトちゃん",
  bio: "プログラマーです"
};

オプショナルプロパティの型

オプショナルプロパティにアクセスすると、その型はT | undefinedとなります。strictNullChecksが有効な場合、undefinedの可能性を考慮する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface UserProfile {
  name: string;
  nickname?: string;
}

function displayNickname(user: UserProfile): void {
  // user.nickname は string | undefined 型
  console.log(user.nickname.toUpperCase());
  // Error: 'user.nickname' is possibly 'undefined'.
}

// 安全なアクセス方法1: 条件分岐
function displayNicknameSafe1(user: UserProfile): void {
  if (user.nickname !== undefined) {
    console.log(user.nickname.toUpperCase());
  } else {
    console.log("ニックネームは未設定です");
  }
}

// 安全なアクセス方法2: オプショナルチェイニング
function displayNicknameSafe2(user: UserProfile): void {
  console.log(user.nickname?.toUpperCase() ?? "ニックネームは未設定です");
}

// 安全なアクセス方法3: デフォルト値
function displayNicknameSafe3(user: UserProfile): void {
  const nickname = user.nickname ?? "匿名";
  console.log(nickname.toUpperCase());
}

関数パラメータでのデフォルト値

オプショナルプロパティに対して、分割代入でデフォルト値を設定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface DrawOptions {
  color?: string;
  lineWidth?: number;
}

function draw({ color = "black", lineWidth = 1 }: DrawOptions): void {
  console.log(`色: ${color}, 線幅: ${lineWidth}`);
}

draw({});                       // "色: black, 線幅: 1"
draw({ color: "red" });         // "色: red, 線幅: 1"
draw({ lineWidth: 3 });         // "色: black, 線幅: 3"
draw({ color: "blue", lineWidth: 2 }); // "色: blue, 線幅: 2"

読み取り専用プロパティ - 変更を防ぐreadonly

readonly修飾子を付けたプロパティは、初期化後に再代入できなくなります。イミュータブル(不変)なデータを扱う際に便利です。

readonlyの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface Point {
  readonly x: number;
  readonly y: number;
}

const origin: Point = { x: 0, y: 0 };

// 読み取りはOK
console.log(origin.x); // 0
console.log(origin.y); // 0

// 再代入はエラー
origin.x = 10;
// Error: Cannot assign to 'x' because it is a read-only property.

readonlyの注意点

readonlyはプロパティ自体の再代入を防ぎますが、プロパティがオブジェクトや配列の場合、その中身の変更は防げません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface User {
  readonly id: string;
  readonly profile: {
    name: string;
    age: number;
  };
}

const user: User = {
  id: "U001",
  profile: {
    name: "田中",
    age: 25
  }
};

// プロパティ自体の再代入は不可
user.profile = { name: "佐藤", age: 30 };
// Error: Cannot assign to 'profile' because it is a read-only property.

// しかし、ネストしたプロパティの変更は可能
user.profile.name = "佐藤"; // OK
user.profile.age = 30;      // OK

ネストしたオブジェクトも完全に読み取り専用にしたい場合は、ネストした型にもreadonlyを付けるか、ユーティリティ型のReadonly<T>を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface UserProfile {
  readonly name: string;
  readonly age: number;
}

interface User {
  readonly id: string;
  readonly profile: UserProfile;
}

// または Readonly ユーティリティ型を使用
type DeepReadonlyUser = {
  readonly id: string;
  readonly profile: Readonly<{
    name: string;
    age: number;
  }>;
};

readonlyと型の互換性

readonlyプロパティを持つ型と、そうでない型は互換性があります。ただし、代入方向によって動作が異なります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface Mutable {
  x: number;
}

interface Immutable {
  readonly x: number;
}

let mutablePoint: Mutable = { x: 10 };
let immutablePoint: Immutable = { x: 20 };

// Mutable → Immutable への代入は可能
immutablePoint = mutablePoint; // OK

// Immutable → Mutable への代入も可能(注意が必要)
mutablePoint = immutablePoint; // OK
mutablePoint.x = 30; // 変更可能になってしまう

この挙動により、readonlyは型チェック時の安全性を提供しますが、実行時の不変性は保証しません。

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

事前にすべてのプロパティ名がわからない場合、インデックスシグネチャを使って動的なキーを持つオブジェクトを型付けできます。

インデックスシグネチャの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 文字列キーと数値値を持つ辞書型
interface NumberDictionary {
  [key: string]: number;
}

const scores: NumberDictionary = {
  math: 85,
  english: 92,
  science: 78
};

// 動的なキーでアクセス
console.log(scores["math"]);    // 85
console.log(scores.english);    // 92

// 新しいキーの追加も可能
scores["history"] = 88;

インデックスシグネチャで使用可能なキー型

インデックスシグネチャのキーとして使用できる型は限られています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 文字列キー
interface StringIndex {
  [key: string]: unknown;
}

// 数値キー
interface NumberIndex {
  [index: number]: string;
}

// シンボルキー
interface SymbolIndex {
  [key: symbol]: boolean;
}

// テンプレートリテラル型(TypeScript 4.4以降)
interface DataAttributes {
  [key: `data-${string}`]: string;
}

const attrs: DataAttributes = {
  "data-id": "123",
  "data-name": "test"
};

固定プロパティとの組み合わせ

インデックスシグネチャと固定プロパティを組み合わせる場合、固定プロパティの型はインデックスシグネチャの型と互換性がなければなりません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface UserData {
  // 固定プロパティ
  id: string;
  name: string;
  // インデックスシグネチャ(追加の任意プロパティ)
  [key: string]: string;
}

const user: UserData = {
  id: "U001",
  name: "田中",
  email: "tanaka@example.com",  // 追加プロパティもOK
  department: "開発部"
};

固定プロパティの型がインデックスシグネチャの型と一致しない場合はエラーになります。

1
2
3
4
interface InvalidData {
  id: number;  // Error: 'number' is not assignable to 'string'
  [key: string]: string;
}

この問題を解決するには、インデックスシグネチャの値型をユニオン型にします。

1
2
3
4
5
interface UserData {
  id: number;
  name: string;
  [key: string]: string | number;  // 両方の型を許容
}

読み取り専用のインデックスシグネチャ

インデックスシグネチャにもreadonlyを付けて、追加・変更を防ぐことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface ReadonlyDictionary {
  readonly [key: string]: number;
}

const constants: ReadonlyDictionary = {
  PI: 3.14159,
  E: 2.71828
};

constants["PI"] = 3.14;  // Error: Index signature in type 'ReadonlyDictionary' only permits reading.
constants["NEW"] = 1.41; // Error: 同上

実践的なオブジェクト型の設計パターン

APIレスポンスの型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// APIレスポンスの共通構造
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: {
    code: string;
    message: string;
  };
}

// ユーザーデータの型
interface UserData {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

// ユーザー一覧APIのレスポンス型
interface UserListResponse extends ApiResponse<UserData[]> {
  pagination: {
    page: number;
    limit: number;
    total: number;
  };
}

// 使用例
function processUserList(response: UserListResponse): void {
  if (response.success) {
    response.data.forEach(user => {
      console.log(`${user.name} (${user.email})`);
    });
    console.log(`全${response.pagination.total}件中、${response.data.length}件を表示`);
  } else {
    console.error(`エラー: ${response.error?.message}`);
  }
}

フォームデータの型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface FormField<T> {
  value: T;
  error?: string;
  touched: boolean;
}

interface LoginForm {
  email: FormField<string>;
  password: FormField<string>;
  rememberMe: FormField<boolean>;
}

const loginForm: LoginForm = {
  email: {
    value: "",
    touched: false
  },
  password: {
    value: "",
    touched: false
  },
  rememberMe: {
    value: false,
    touched: false
  }
};

設定オブジェクトの型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface DatabaseConfig {
  readonly host: string;
  readonly port: number;
  readonly database: string;
  readonly user: string;
  readonly password: string;
  ssl?: boolean;
  poolSize?: number;
}

interface AppConfig {
  readonly env: "development" | "staging" | "production";
  readonly database: DatabaseConfig;
  readonly api: {
    readonly baseUrl: string;
    readonly timeout: number;
  };
  features?: {
    [featureName: string]: boolean;
  };
}

const config: AppConfig = {
  env: "development",
  database: {
    host: "localhost",
    port: 5432,
    database: "myapp",
    user: "admin",
    password: "secret",
    ssl: false
  },
  api: {
    baseUrl: "http://localhost:3000",
    timeout: 5000
  },
  features: {
    darkMode: true,
    betaFeatures: false
  }
};

よくある間違いと対処法

余剰プロパティチェックの回避

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface User {
  name: string;
  age: number;
}

// オブジェクトリテラルを直接渡すとエラー
const user: User = {
  name: "田中",
  age: 25,
  email: "tanaka@example.com" // Error
};

// 型アサーションで回避(非推奨)
const user2 = {
  name: "田中",
  age: 25,
  email: "tanaka@example.com"
} as User;

// 推奨: 型定義を見直す
interface UserWithEmail extends User {
  email: string;
}

undefinedとオプショナルの違い

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface Example {
  optional?: string;        // プロパティ自体が存在しなくてもOK
  maybeUndefined: string | undefined;  // プロパティは必須だが値がundefinedでもOK
}

const example1: Example = {
  maybeUndefined: undefined
}; // OK - optionalは省略可能

const example2: Example = {
  optional: undefined,
  maybeUndefined: "value"
}; // OK

// const example3: Example = {};
// Error: Property 'maybeUndefined' is missing

まとめ

本記事では、TypeScriptのオブジェクト型について解説しました。

  • オブジェクト型リテラルは、インラインで型を定義する方法です。一度しか使わない型や小規模な型定義に適しています
  • interfaceは、再利用可能な名前付きオブジェクト型を定義します。extendsによる継承や宣言のマージが可能です
  • **オプショナルプロパティ(?)**で、省略可能なプロパティを定義できます。アクセス時はundefinedの可能性を考慮しましょう
  • readonly修飾子で、再代入を防ぐ読み取り専用プロパティを定義できます。ただし、ネストしたオブジェクトの中身は保護されません
  • インデックスシグネチャで、動的なキーを持つオブジェクトを型付けできます

これらの機能を組み合わせることで、現実のアプリケーションで扱う複雑なデータ構造を型安全に表現できます。

次回は、typeエイリアスとinterfaceの違いについて詳しく解説します。

参考リンク