12k
All articles

TypeScriptの辞書: 型安全なオブジェクトの完全ガイド

インデックスシグネチャ、Recordタイプ、Mapを比較しながら、TypeScriptで型安全なキーと値の構造を構築し、ランタイムエラーを減らす方法を解説する。

OpenReplay Team
OpenReplay Team
TypeScriptの辞書: 型安全なオブジェクトの完全ガイド

TypeScriptでキーと値のペアを扱う場合、データへのアクセスと操作において型安全性を維持する信頼性の高い方法が必要です。組み込みの辞書型を持つ言語とは異なり、TypeScriptでは型安全な辞書を作成するために特定のアプローチが必要です。このガイドでは、すべての実装オプションを探り、プロジェクトに適したアプローチを選ぶのに役立つ実用的な例を紹介します。

重要なポイント

  • TypeScriptは辞書を実装するための複数の方法を提供: インデックスシグネチャ、Recordユーティリティ、Map
  • 必要に応じて実装を選択: 単純な辞書にはインデックスシグネチャ、特定のキーセットにはRecord、高度な機能にはMap
  • TypeScriptの型システムを活用して、辞書操作のコンパイル時の安全性を確保
  • オブジェクトベースの辞書とMapの間で選択する際にパフォーマンスへの影響を考慮
  • 複雑な辞書要件にはマップ型などの高度な型テクニックを使用

TypeScriptの辞書とは?

TypeScriptにおける辞書は、キーと値のペアを格納し、キーによる効率的な検索を可能にするデータ構造です。TypeScriptにはネイティブのDictionary型はありませんが、完全な型安全性を備えた辞書のような構造を実装するための複数の方法が提供されています。

型安全な辞書は以下を保証します:

  • キーと値が特定の型に準拠する
  • 型の不一致に対するコンパイル時のエラー
  • オートコンプリートとIntelliSenseのサポート
  • 実行時エラーからの保護

TypeScriptにおける辞書実装オプション

インデックスシグネチャの使用

最も直接的なアプローチは、TypeScriptのインデックスシグネチャを持つJavaScriptオブジェクトを使用することです:

// 基本的なインデックスシグネチャ辞書
const userScores: { [username: string]: number } = {};

// エントリの追加
userScores[""alice""] = 95;
userScores[""bob""] = 87;

// 値へのアクセス
console.log(userScores[""alice""]); // 95

// 型安全性の動作
userScores[""charlie""] = ""high""; // エラー: 型 'string' を型 'number' に割り当てることはできません

このアプローチはシンプルですが、コードベース全体で再利用する場合は扱いにくくなります。

再利用可能な辞書型の作成

より良い再利用性のために、ジェネリック辞書型を定義します:

// ジェネリック辞書型
type Dictionary<K extends string | number | symbol, V> = {
  [key in K]: V;
};

// 文字列キーと数値値での使用
const scores: Dictionary<string, number> = {};
scores[""math""] = 95;
scores[""science""] = 87;

// 数値キーでの使用
const idMapping: Dictionary<number, string> = {};
idMapping[1] = ""user_a"";
idMapping[2] = ""user_b"";

Recordユーティリティ型の使用

TypeScriptの組み込みRecordユーティリティ型はより簡潔な構文を提供します:

// 固定されたキーセットにRecordを使用
type UserFields = ""name"" | ""email"" | ""role"";
const user: Record<UserFields, string> = {
  name: ""Alice Smith"",
  email: ""alice@example.com"",
  role: ""Admin""
};

// 任意の文字列キーでRecordを使用
const config: Record<string, any> = {};
config[""apiUrl""] = ""https://api.example.com"";
config[""timeout""] = 5000;

Record型は、文字列リテラル型を使用して特定のキーセットを強制する必要がある場合に特に便利です。

TypeScriptでJavaScriptのMapを使用

より高度な機能には、TypeScriptのジェネリクスを使用したJavaScriptのMapオブジェクトを使用します:

// 型安全なMap
const userProfiles = new Map<string, {age: number, active: boolean}>();

// エントリの追加
userProfiles.set(""alice"", {age: 28, active: true});
userProfiles.set(""bob"", {age: 34, active: false});

// 型チェックが機能する
userProfiles.set(""charlie"", {age: ""thirty""}); // エラー: 型 'string' を型 'number' に割り当てることはできません

// 値へのアクセス
const aliceProfile = userProfiles.get(""alice"");
console.log(aliceProfile?.age); // 28

辞書実装の比較

機能 インデックスシグネチャを持つオブジェクト Record型 Map キーの型 文字列、数値、シンボル 任意の型 任意の型 パフォーマンス 小さなデータセットで高速 小さなデータセットで高速 頻繁な追加/削除に適している メモリ使用量 低い 低い 高い 順序の保持 いいえ いいえ はい(挿入順) 反復処理 Object.entries()が必要 Object.entries()が必要 組み込みイテレータ 特別なメソッド いいえ いいえ has(), delete(), clear() プロトタイプ汚染リスク はい はい いいえ

一般的な辞書操作

キーの存在確認

// インデックスシグネチャの使用
const hasKey = (dict: Record<string, unknown>, key: string): boolean => {
  return key in dict;
};

// Mapの使用
const userMap = new Map<string, number>();
userMap.has(""alice""); // true/false

値の追加と更新

// インデックスシグネチャの使用
type UserDict = Record<string, {name: string, role: string}>;
const users: UserDict = {};

// 追加
users[""u1""] = {name: ""Alice"", role: ""Admin""};

// 更新
users[""u1""] = {...users[""u1""], role: ""User""};

// Mapの使用
const userMap = new Map<string, {name: string, role: string}>();
userMap.set(""u1"", {name: ""Alice"", role: ""Admin""});
userMap.set(""u1"", {name: ""Alice"", role: ""User""}); // 既存のエントリを更新

キーの削除

// インデックスシグネチャの使用
delete users[""u1""];

// Mapの使用
userMap.delete(""u1"");

辞書の反復処理

// オブジェクトベースの辞書の反復処理
const scores: Record<string, number> = {math: 95, science: 87};

// 方法1: Object.entries()
for (const [subject, score] of Object.entries(scores)) {
  console.log(`${subject}: ${score}`);
}

// 方法2: Object.keys()
Object.keys(scores).forEach(subject => {
  console.log(`${subject}: ${scores[subject]}`);
});

// Mapの反復処理
const scoreMap = new Map<string, number>([
  [""math"", 95],
  [""science"", 87]
]);

// 直接反復処理
for (const [subject, score] of scoreMap) {
  console.log(`${subject}: ${score}`);
}

// forEachの使用
scoreMap.forEach((score, subject) => {
  console.log(`${subject}: ${score}`);
});

高度な型安全性テクニック

キータイプの制約

// 文字列リテラル共用体型の使用
type ValidKeys = ""id"" | ""name"" | ""email"";
const userData: Record<ValidKeys, string> = {
  id: ""123"",
  name: ""Alice"",
  email: ""alice@example.com""
};

// 無効なキーの使用を試みる
userData.phone = ""555-1234""; // エラー: プロパティ 'phone' は存在しません

読み取り専用辞書

// 不変辞書
const constants: Readonly<Record<string, number>> = {
  MAX_USERS: 100,
  TIMEOUT_MS: 5000
};

constants.MAX_USERS = 200; // エラー: 'MAX_USERS' は読み取り専用プロパティであるため、割り当てることができません

部分的辞書

// オプション値を持つ辞書
type UserProfile = {
  name: string;
  email: string;
  age: number;
};

// 各値がUserProfileのプロパティの一部または全部を持つ辞書を作成
const profiles: Record<string, Partial<UserProfile>> = {};

profiles[""alice""] = { name: ""Alice"" }; // emailとageがなくても有効
profiles[""bob""] = { name: ""Bob"", email: ""bob@example.com"" }; // 有効

高度な辞書のためのマップ型の使用

type SensorData = {
  temperature: number;
  humidity: number;
  pressure: string;
  metadata: object;
};

// SensorDataから数値プロパティのみを含む辞書型を作成
type NumericSensorDict = {
  [K in keyof SensorData as SensorData[K] extends number ? K : never]: boolean;
};

// 結果の型はtemperatureとhumidityのみを有効なキーとして持つ
const sensorStatus: NumericSensorDict = {
  temperature: true,
  humidity: false
};

パフォーマンスに関する考慮事項

辞書実装を選択する際には、以下を考慮してください:

  1. データサイズ: 文字列キーを持つ小さなデータセットでは、オブジェクトベースの辞書が効率的
  2. 頻繁な変更: 頻繁な追加と削除にはMapの方がパフォーマンスが良い
  3. メモリ使用量: オブジェクトベースの辞書はMapよりメモリ使用量が少ない
  4. キーの型: 文字列以外のキーが必要な場合はMapを使用

10,000操作のベンチマーク結果:

オブジェクトアクセス: ~25ms
Mapアクセス: ~45ms
オブジェクト挿入: ~30ms
Map挿入: ~50ms
オブジェクト削除: ~20ms
Map削除: ~15ms

実世界のユースケース

設定管理

type AppConfig = Record<string, string | number | boolean>;

const config: AppConfig = {
  apiUrl: ""https://api.example.com"",
  timeout: 5000,
  enableCache: true
};

キャッシュ実装

class Cache<T> {
  private store = new Map<string, {value: T, expiry: number}>();
  
  set(key: string, value: T, ttlSeconds: number): void {
    const expiry = Date.now() + (ttlSeconds * 1000);
    this.store.set(key, {value, expiry});
  }
  
  get(key: string): T | null {
    const item = this.store.get(key);
    
    if (!item) return null;
    if (item.expiry < Date.now()) {
      this.store.delete(key);
      return null;
    }
    
    return item.value;
  }
}

const userCache = new Cache<{name: string, role: string}>();
userCache.set(""user:123"", {name: ""Alice"", role: ""Admin""}, 300);

Reactでの状態管理

interface UserState {
  users: Record<string, {name: string, role: string}>;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  users: {},
  loading: false,
  error: null
};

// リデューサー内で
function userReducer(state = initialState, action: any): UserState {
  switch (action.type) {
    case 'ADD_USER':
      return {
        ...state,
        users: {
          ...state.users,
          [action.payload.id]: {
            name: action.payload.name,
            role: action.payload.role
          }
        }
      };
    // 他のケース...
    default:
      return state;
  }
}

エラー処理戦略

存在しないキーの処理

function safeGet<K extends string, V>(
  dict: Record<K, V>, 
  key: string, 
  defaultValue: V
): V {
  return (key in dict) ? dict[key as K] : defaultValue;
}

const users: Record<string, string> = {
  ""1"": ""Alice""
};

const userName = safeGet(users, ""2"", ""Unknown User"");
console.log(userName); // ""Unknown User""

辞書値の型ガード

function isUserDict(obj: unknown): obj is Record<string, {name: string, age: number}> {
  if (typeof obj !== 'object' || obj === null) return false;
  
  for (const key in obj as object) {
    const value = (obj as any)[key];
    if (typeof value !== 'object' || 
        typeof value.name !== 'string' || 
        typeof value.age !== 'number') {
      return false;
    }
  }
  
  return true;
}

// 使用法
function processUserData(data: unknown) {
  if (isUserDict(data)) {
    // TypeScriptはdataがRecord<string, {name: string, age: number}>であることを認識
    for (const [id, user] of Object.entries(data)) {
      console.log(`User ${id}: ${user.name}, ${user.age}`);
    }
  }
}

結論

TypeScriptの辞書は、完全な型安全性を備えたキーと値のデータを管理するための強力な方法を提供します。特定の要件に基づいて適切な実装アプローチを選択することで、実行時エラーの少ない堅牢なアプリケーションを構築できます。単純な文字列キーのオブジェクトが必要な場合でも、高度な機能を持つ複雑な辞書が必要な場合でも、TypeScriptの型システムにより、辞書操作がコードベース全体で型安全であることが保証されます。

よくある質問

オブジェクトベースの辞書の代わりにMapを使用するのはいつですか?

文字列以外のキー、保証されたキー順序の保持、頻繁な追加と削除、またはプロトタイプ汚染からの保護が必要な場合はMapを使用してください。Mapは特定の操作をより便利にするhas()、delete()、clear()などの組み込みメソッドを提供します。

固定されたキーセットを持つ辞書を作成するにはどうすればよいですか?

文字列リテラル型をRecordユーティリティ型と共に使用します:nn`typescriptntype AllowedKeys = 'id' | 'name' | 'email';nconst user: Record<AllowedKeys, string> = {n id: '123',n name: 'Alice',n email: 'alice@example.com'n};n`nnこれにより、AllowedKeys共用体にないキーを使用しようとするとTypeScriptがエラーを出します。

TypeScriptの辞書でキーとして複雑なオブジェクトを使用できますか?

オブジェクトベースの辞書ではできませんが、Mapでは可能です:nn`typescriptnconst userMap = new Map<{id: number}, string>();nconst key1 = {id: 1};nconst key2 = {id: 1}; // 異なるオブジェクト参照nnuserMap.set(key1, 'Alice');nconsole.log(userMap.get(key1)); // 'Alice'nconsole.log(userMap.get(key2)); // undefined(異なるオブジェクト参照)n`nnMapはオブジェクトキーに対して参照等価性を使用することに注意してください。

オブジェクト辞書とMapの間でどのように変換しますか?

オブジェクトからMapへの変換:nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nnMapからオブジェクトへの変換:nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n`

辞書値にアクセスする際にnull安全性を確保するにはどうすればよいですか?

オプショナルチェーンとnullish合体演算子を使用します:nn`typescriptnconst users: Record<string, {name: string} | undefined> = {n '1': {name: 'Alice'}n};nn// フォールバック付きの安全なアクセスnconst userName = users['2']?.name ?? 'Unknown';nconsole.log(userName); // 'Unknown'n`nnこれにより、潜在的に未定義の値のプロパティにアクセスする際の実行時エラーを防ぎます。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.