Avatar ·

Как правильно использовать Map из ES6 в TypeScript? Типизация и примеры

Всем привет! Начал изучать TypeScript после JavaScript и столкнулся с проблемой при использовании Map. В JS я спокойно писал: ```typescript let map = new Map(); map.set('name', 'John'); map.set(42, 'answer'); ``` Но в TypeScript это вызывает ошибки компиляции, если не указать типы. Я пробовал разные варианты, но постоянно путаюсь: 1. Просто `new Map()` — работает, но TypeScript ругается на `any` 2. Пытался указать `Map` — вроде работает, но теряется смысл типизации 3. Хочу хранить объекты с разными ключами и значениями, но не знаю, как правильно описать типы Конкретная задача: нужно создать Map, где ключи — строки, а значения — объекты с полями id (number) и name (string). Также хочу понять, можно ли использовать в качестве ключей объекты или функции, как в ES6. Подскажите, пожалуйста, как правильно типизировать Map в TypeScript? Какие есть best practices для разных случаев? И как избежать ошибок, когда ключи или значения могут быть разных типов? Заранее спасибо за помощь! <<>>

Основы типизации Map в TypeScript

В TypeScript Map — это дженерик, который принимает два параметра типа: тип ключа и тип значения. Базовая запись выглядит так:

const map = new Map<string, number>();
map.set('key', 42); // OK
map.set(123, 'value'); // Ошибка: ключ должен быть string, значение — number

Решение вашей задачи

Для хранения объектов с полями id и name:

interface User {
    id: number;
    name: string;
}

const userMap = new Map<string, User>();
userMap.set('john', { id: 1, name: 'John Doe' });
userMap.set('jane', { id: 2, name: 'Jane Smith' });

// Получение значения
const user = userMap.get('john');
if (user) {
    console.log(user.name); // TypeScript знает, что это User
}

Работа с разными типами ключей и значений

Если нужно хранить значения разных типов, используйте объединения (union types) или any с осторожностью:

// Строгий вариант с union
const mixedMap = new Map<string, string | number | boolean>();
mixedMap.set('name', 'Alice');
mixedMap.set('age', 30);
mixedMap.set('active', true);

// Или с конкретными типами через keyof
type ValueType = {
    name: string;
    age: number;
    active: boolean;
};

const typedMap = new Map<keyof ValueType, ValueType[keyof ValueType]>();
typedMap.set('name', 'Bob');
typedMap.set('age', 25);

Объекты и функции как ключи

TypeScript поддерживает использование объектов и функций в качестве ключей, но нужно учитывать особенности:

// Объекты как ключи
const objKeyMap = new Map<object, string>();
const key1 = { id: 1 };
const key2 = { id: 2 };
objKeyMap.set(key1, 'value1');
objKeyMap.set(key2, 'value2');

// Функции как ключи
const fnMap = new Map<Function, string>();
function foo() {}
fnMap.set(foo, 'function foo');

// Важно: ключи сравниваются по ссылке, не по значению!
const sameKey = { id: 1 };
console.log(objKeyMap.get(sameKey)); // undefined — другой объект!

Best practices

  • Всегда указывайте типы — это главное преимущество TypeScript
  • Используйте интерфейсы для сложных типов значений
  • Проверяйте наличие ключа через has() или optional chaining
  • Для readonly Map используйте ReadonlyMap
  • Избегайте any — это убивает смысл типизации
// Пример с ReadonlyMap
function processData(map: ReadonlyMap<string, User>) {
    // map.set() — ошибка компиляции, только чтение
    const user = map.get('john');
}

// Итерация по Map
for (const [key, value] of userMap) {
    console.log(`${key}: ${value.name}`);
}

// Преобразование в массив
const entries = Array.from(userMap.entries());
const keys = Array.from(userMap.keys());

Обработка отсутствующих значений

// Безопасное получение с default
const user = userMap.get('unknown') ?? { id: 0, name: 'Default' };

// Или через has()
if (userMap.has('john')) {
    const user = userMap.get('john')!; // Non-null assertion (осторожно!)
}

Используя эти подходы, вы получите строгую типизацию и избежите ошибок времени выполнения. TypeScript будет проверять соответствие типов на этапе компиляции.

<<>>

Альтернативный подход: Record и утилитарные типы

В некоторых случаях вместо Map удобнее использовать встроенный тип Record или объекты с индексацией. Это особенно актуально, когда ключи — строки или числа, и не нужны методы Map (например, итерация по порядку вставки).

// Record вместо Map
type UserMap = Record<string, User>;
const users: UserMap = {
    'john': { id: 1, name: 'John' },
    'jane': { id: 2, name: 'Jane' }
};

// Или с Partial для опциональных значений
type OptionalUserMap = Partial<Record<string, User>>;
const optionalUsers: OptionalUserMap = {
    'john': { id: 1, name: 'John' }
    // 'jane' может отсутствовать
};

Когда использовать Record вместо Map

  • Ключи — простые строки или числа
  • Не нужны методы Map (clear, delete, size)
  • Нужна сериализация в JSON (Map не сериализуется напрямую)
  • Производительность: объекты быстрее для простых операций

Комбинированный подход

Можно использовать Map для хранения, а для передачи данных преобразовывать в Record:

function mapToRecord<K extends string, V>(map: Map<K, V>): Record<K, V> {
    const record = {} as Record<K, V>;
    for (const [key, value] of map) {
        record[key] = value;
    }
    return record;
}

Этот подход сохраняет все преимущества Map (произвольные ключи, методы) и позволяет легко интегрироваться с API, ожидающими объекты.