Словарь TypeScript: Полное руководство по типобезопасным объектам

При работе с парами ключ-значение в TypeScript вам нужен надежный способ поддерживать типобезопасность при доступе и манипулировании данными. В отличие от языков со встроенными типами словарей, TypeScript требует специальных подходов для создания типобезопасных словарей. Это руководство исследует все варианты реализации с практическими примерами, которые помогут вам выбрать правильный подход для ваших проектов.
Ключевые моменты
- TypeScript предлагает несколько способов реализации словарей: сигнатуры индексов, утилита
Record
иMap
- Выбирайте реализацию в зависимости от ваших потребностей: сигнатуры индексов для простых словарей,
Record
для конкретных наборов ключей иMap
для расширенных функций - Используйте систему типов TypeScript для обеспечения безопасности времени компиляции для операций со словарями
- Учитывайте влияние на производительность при выборе между словарями на основе объектов и Map
- Используйте продвинутые методы типизации, такие как сопоставленные типы, для сложных требований к словарям
Что такое словарь в TypeScript?
Словарь в TypeScript — это структура данных, которая хранит пары ключ-значение, позволяя эффективно выполнять поиск по ключу. Хотя в TypeScript нет встроенного типа Dictionary
, он предоставляет несколько способов реализации структур, подобных словарям, с полной типобезопасностью.
Типобезопасный словарь обеспечивает:
- Соответствие ключей и значений определенным типам
- Ошибки времени компиляции при несоответствии типов
- Поддержку автозаполнения и IntelliSense
- Защиту от ошибок времени выполнения
Варианты реализации словаря в TypeScript
Использование сигнатур индексов
Самый простой подход использует объекты JavaScript с сигнатурами индексов TypeScript:
// Basic index signature dictionary
const userScores: { [username: string]: number } = {};
// Adding entries
userScores[""alice""] = 95;
userScores[""bob""] = 87;
// Accessing values
console.log(userScores[""alice""]); // 95
// Type safety in action
userScores[""charlie""] = ""high""; // Error: Type 'string' is not assignable to type 'number'
Этот подход прост, но становится громоздким при повторном использовании в вашей кодовой базе.
Создание многоразового типа словаря
Для лучшей повторной используемости определите универсальный тип словаря:
// Generic dictionary type
type Dictionary<K extends string | number | symbol, V> = {
[key in K]: V;
};
// Usage with string keys and number values
const scores: Dictionary<string, number> = {};
scores[""math""] = 95;
scores[""science""] = 87;
// Usage with numeric keys
const idMapping: Dictionary<number, string> = {};
idMapping[1] = ""user_a"";
idMapping[2] = ""user_b"";
Использование утилиты Record
Встроенный в TypeScript тип утилиты Record
обеспечивает более чистый синтаксис:
// Using Record for fixed set of keys
type UserFields = ""name"" | ""email"" | ""role"";
const user: Record<UserFields, string> = {
name: ""Alice Smith"",
email: ""alice@example.com"",
role: ""Admin""
};
// Using Record with any string keys
const config: Record<string, any> = {};
config[""apiUrl""] = ""https://api.example.com"";
config[""timeout""] = 5000;
Тип Record
особенно полезен, когда вам нужно обеспечить конкретный набор ключей, используя строковые литеральные типы.
Использование Map из JavaScript с TypeScript
Для более продвинутых функций используйте объект Map
из JavaScript с дженериками TypeScript:
// Type-safe Map
const userProfiles = new Map<string, {age: number, active: boolean}>();
// Adding entries
userProfiles.set(""alice"", {age: 28, active: true});
userProfiles.set(""bob"", {age: 34, active: false});
// Type checking works
userProfiles.set(""charlie"", {age: ""thirty""}); // Error: Type 'string' not assignable to type 'number'
// Accessing values
const aliceProfile = userProfiles.get(""alice"");
console.log(aliceProfile?.age); // 28
Сравнение реализаций словарей
Функция Объект с сигнатурой индекса Тип Record Map Типы ключей Строка, число, символ Любой тип Любой тип Производительность Быстро для малых наборов данных Быстро для малых наборов данных Лучше для частых добавлений/удалений Использование памяти Ниже Ниже Выше Сохранение порядка Нет Нет Да (порядок вставки) Итерация Требует Object.entries()
Требует Object.entries()
Встроенные итераторы Специальные методы Нет Нет has()
, delete()
, clear()
Риск загрязнения прототипа Да Да Нет
Общие операции со словарями
Проверка наличия ключа
// Using index signature
const hasKey = (dict: Record<string, unknown>, key: string): boolean => {
return key in dict;
};
// Using Map
const userMap = new Map<string, number>();
userMap.has(""alice""); // true/false
Добавление и обновление значений
// Using index signature
type UserDict = Record<string, {name: string, role: string}>;
const users: UserDict = {};
// Adding
users[""u1""] = {name: ""Alice"", role: ""Admin""};
// Updating
users[""u1""] = {...users[""u1""], role: ""User""};
// Using Map
const userMap = new Map<string, {name: string, role: string}>();
userMap.set(""u1"", {name: ""Alice"", role: ""Admin""});
userMap.set(""u1"", {name: ""Alice"", role: ""User""}); // Updates existing entry
Удаление ключей
// Using index signature
delete users[""u1""];
// Using Map
userMap.delete(""u1"");
Итерация по словарям
// Object-based dictionary iteration
const scores: Record<string, number> = {math: 95, science: 87};
// Method 1: Object.entries()
for (const [subject, score] of Object.entries(scores)) {
console.log(`${subject}: ${score}`);
}
// Method 2: Object.keys()
Object.keys(scores).forEach(subject => {
console.log(`${subject}: ${scores[subject]}`);
});
// Map iteration
const scoreMap = new Map<string, number>([
[""math"", 95],
[""science"", 87]
]);
// Direct iteration
for (const [subject, score] of scoreMap) {
console.log(`${subject}: ${score}`);
}
// Using forEach
scoreMap.forEach((score, subject) => {
console.log(`${subject}: ${score}`);
});
Продвинутые методы обеспечения типобезопасности
Ограничение типов ключей
// Using string literal union types
type ValidKeys = ""id"" | ""name"" | ""email"";
const userData: Record<ValidKeys, string> = {
id: ""123"",
name: ""Alice"",
email: ""alice@example.com""
};
// Attempting to use invalid keys
userData.phone = ""555-1234""; // Error: Property 'phone' does not exist
Словари только для чтения
// Immutable dictionary
const constants: Readonly<Record<string, number>> = {
MAX_USERS: 100,
TIMEOUT_MS: 5000
};
constants.MAX_USERS = 200; // Error: Cannot assign to 'MAX_USERS' because it is a read-only property
Частичные словари
// Dictionary with optional values
type UserProfile = {
name: string;
email: string;
age: number;
};
// Create a dictionary where each value can have some or all UserProfile properties
const profiles: Record<string, Partial<UserProfile>> = {};
profiles[""alice""] = { name: ""Alice"" }; // Valid even without email and age
profiles[""bob""] = { name: ""Bob"", email: ""bob@example.com"" }; // Valid
Использование сопоставленных типов для продвинутых словарей
type SensorData = {
temperature: number;
humidity: number;
pressure: string;
metadata: object;
};
// Create a dictionary type that only includes number properties from SensorData
type NumericSensorDict = {
[K in keyof SensorData as SensorData[K] extends number ? K : never]: boolean;
};
// The resulting type only has temperature and humidity as valid keys
const sensorStatus: NumericSensorDict = {
temperature: true,
humidity: false
};
Соображения производительности
При выборе реализации словаря учитывайте:
- Размер данных: для небольших наборов данных со строковыми ключами эффективны словари на основе объектов
- Частые модификации:
Map
работает лучше при частых добавлениях и удалениях - Использование памяти: словари на основе объектов используют меньше памяти, чем
Map
- Типы ключей: если вам нужны нестроковые ключи, используйте
Map
Результаты тестирования для 10 000 операций:
Object access: ~25ms
Map access: ~45ms
Object insertion: ~30ms
Map insertion: ~50ms
Object deletion: ~20ms
Map deletion: ~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
};
// In a reducer
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
}
}
};
// Other cases...
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;
}
// Usage
function processUserData(data: unknown) {
if (isUserDict(data)) {
// TypeScript knows data is 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, когда вам нужны нестроковые ключи, гарантированное сохранение порядка ключей, частые добавления и удаления или защита от загрязнения прототипа. Maps предоставляют встроенные методы, такие как 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Это гарантирует, что TypeScript выдаст ошибку, если вы попытаетесь использовать ключи, не входящие в объединение AllowedKeys
Не с словарями на основе объектов, но можно с 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`nnОбратите внимание, что Map использует равенство по ссылке для ключей-объектов
Преобразование объекта в Map:nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nnПреобразование Map в объект:nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n`
Используйте опциональную цепочку и оператор нулевого слияния: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Это предотвращает ошибки времени выполнения при доступе к свойствам потенциально неопределенных значений