Avatar ·

Использование объектов TypeScript в качестве словарей (Dictionary) как в C#

Привет! Я перехожу с C# на TypeScript и никак не могу понять, как правильно реализовать аналог Dictionary. В C# я пишу: ```csharp var dict = new Dictionary(); dict.Add("apple", 5); dict.Add("banana", 3); Console.WriteLine(dict["apple"]); // 5 ``` В TypeScript я пытаюсь делать так: ```typescript let dict = {}; dict["apple"] = 5; dict["banana"] = 3; console.log(dict["apple"]); // 5 ``` Но TypeScript ругается на `dict["apple"]` — пишет, что элемент неявно имеет тип `any`. Если я явно типизирую: ```typescript let dict: { [key: string]: number } = {}; ``` То всё работает, но я не понимаю: 1. Как правильно объявить такой словарь? 2. Как проверить, существует ли ключ? 3. Как перебрать все элементы словаря? 4. Есть ли встроенный класс Dictionary в TypeScript? Подскажите, как в TypeScript принято работать с объектами как со словарями? Мне нужно хранить пары "ключ-значение", где ключи — строки, а значения — объекты моего класса `User`. Буду благодарен за примеры с пояснениями. Заранее спасибо! <<>>

Основной подход: использование index signature

В TypeScript для создания словарей используется index signature (сигнатура индекса). Это именно то, что вы уже попробовали:

let dict: { [key: string]: number } = {};

Такая запись означает: «объект, у которого ключи — строки, а значения — числа». Для вашего случая с классом User:

class User {
  constructor(public name: string, public age: number) {}
}

let users: { [key: string]: User } = {};
users["alice"] = new User("Alice", 30);
users["bob"] = new User("Bob", 25);

Более удобная альтернатива: Record<K, V>

TypeScript предоставляет встроенный тип Record<Keys, Type>, который делает код чище:

let users: Record<string, User> = {};
users["alice"] = new User("Alice", 30);

Запись Record<string, User> эквивалентна { [key: string]: User }, но читается проще.

Проверка существования ключа

Есть два основных способа:

  • Оператор in — проверяет наличие ключа в объекте (включая прототип):
if ("alice" in users) {
  console.log(users["alice"].name);
}
  • hasOwnProperty — проверяет только собственные свойства (рекомендуется):
if (users.hasOwnProperty("alice")) {
  console.log(users["alice"].name);
}
  • Optional chaining (ES2020+) — современный способ:
const user = users["alice"]; // если ключа нет, user будет undefined
console.log(user?.name); // безопасный доступ

Перебор элементов словаря

Несколько вариантов для разных задач:

// 1. Только значения
for (const key in users) {
  if (users.hasOwnProperty(key)) {
    console.log(users[key].name);
  }
}

// 2. Ключи и значения через Object.keys
Object.keys(users).forEach(key => {
  console.log(`Ключ: ${key}, Имя: ${users[key].name}`);
});

// 3. Использование Object.entries (ES2017+)
for (const [key, user] of Object.entries(users)) {
  console.log(`${key}: ${user.name}, ${user.age} лет`);
}

// 4. Только значения через Object.values
Object.values(users).forEach(user => {
  console.log(user.name);
});

Удаление элемента

delete users["alice"];

Полноценный класс Dictionary

Если вам нужны методы, как в C# (Add, ContainsKey, Remove), можно создать свой класс:

class Dictionary<TKey, TValue> {
  private items: Record<string, TValue> = {};

  add(key: TKey, value: TValue): void {
    const strKey = String(key);
    if (this.items.hasOwnProperty(strKey)) {
      throw new Error(`Ключ "${strKey}" уже существует`);
    }
    this.items[strKey] = value;
  }

  get(key: TKey): TValue | undefined {
    return this.items[String(key)];
  }

  containsKey(key: TKey): boolean {
    return this.items.hasOwnProperty(String(key));
  }

  remove(key: TKey): boolean {
    const strKey = String(key);
    if (this.items.hasOwnProperty(strKey)) {
      delete this.items[strKey];
      return true;
    }
    return false;
  }

  get count(): number {
    return Object.keys(this.items).length;
  }

  *[Symbol.iterator](): Iterator<[string, TValue]> {
    for (const [key, value] of Object.entries(this.items)) {
      yield [key, value];
    }
  }
}

// Использование:
const dict = new Dictionary<string, User>();
dict.add("alice", new User("Alice", 30));
console.log(dict.containsKey("alice")); // true
console.log(dict.get("alice")?.name); // Alice

Важные замечания

  • Тип ключа — в index signature можно использовать только string, number или symbol. Для других типов придётся приводить к строке.
  • Безопасность — всегда проверяйте наличие ключа перед доступом, иначе получите undefined.
  • Производительность — для больших словарей (сотни тысяч элементов) рассмотрите Map (см. ответ 2).
<<>>

Альтернатива: использование Map вместо объектов

В JavaScript (и TypeScript) есть встроенный класс Map, который часто удобнее обычных объектов для работы со словарями:

let userMap = new Map<string, User>();
userMap.set("alice", new User("Alice", 30));
userMap.set("bob", new User("Bob", 25));

// Проверка наличия
console.log(userMap.has("alice")); // true

// Получение значения
const user = userMap.get("alice"); // User | undefined

// Удаление
userMap.delete("alice");

// Перебор
userMap.forEach((value, key) => {
  console.log(`${key}: ${value.name}`);
});

// Размер
console.log(userMap.size); // количество элементов

Когда выбирать Map вместо объекта

  • Ключи — не строки — Map поддерживает любые типы ключей (объекты, функции, числа).
  • Частые операции вставки/удаления — Map оптимизирован для таких сценариев.
  • Нужен порядок элементов — Map сохраняет порядок вставки, объекты — нет (хотя современные браузеры сохраняют порядок строковых ключей).
  • Нужен метод size — для объектов придётся использовать Object.keys(obj).length.
  • Безопасность прототипа — Map не имеет прототипа, поэтому нет проблем с ключами типа toString или constructor.

Недостатки Map

  • Нет поддержки JSON-сериализации напрямую (нужно преобразовывать).
  • Синтаксис чуть более многословный (.set(), .get() вместо квадратных скобок).
  • Для простых случаев с однотипными строковыми ключами объекты проще.

Рекомендация: для вашей задачи с пользователями отлично подойдёт Map<string, User>. Он даёт чёткий API, как в C#, и избавляет от необходимости писать проверки hasOwnProperty.