Dictionnaire TypeScript : Guide complet pour des objets type-safe

Lorsque vous travaillez avec des paires clé-valeur en TypeScript, vous avez besoin d’une méthode fiable pour maintenir la sécurité des types tout en accédant et en manipulant des données. Contrairement aux langages dotés de types de dictionnaire intégrés, TypeScript nécessite des approches spécifiques pour créer des dictionnaires type-safe. Ce guide explore toutes les options d’implémentation, avec des exemples pratiques pour vous aider à choisir la bonne approche pour vos projets.
Points clés à retenir
- TypeScript offre plusieurs façons d’implémenter des dictionnaires : signatures d’index, utilitaire
Record
etMap
- Choisissez l’implémentation selon vos besoins : signatures d’index pour les dictionnaires simples,
Record
pour des ensembles de clés spécifiques, etMap
pour des fonctionnalités avancées - Exploitez le système de types de TypeScript pour garantir la sécurité à la compilation pour les opérations de dictionnaire
- Tenez compte des implications de performance lors du choix entre les dictionnaires basés sur des objets et Map
- Utilisez des techniques de typage avancées comme les types mappés pour des exigences complexes de dictionnaire
Qu’est-ce qu’un dictionnaire TypeScript ?
Un dictionnaire en TypeScript est une structure de données qui stocke des paires clé-valeur, permettant des recherches efficaces par clé. Bien que TypeScript n’ait pas de type natif Dictionary
, il offre plusieurs façons d’implémenter des structures de type dictionnaire avec une sécurité de type complète.
Un dictionnaire type-safe garantit :
- Les clés et les valeurs sont conformes à des types spécifiques
- Des erreurs à la compilation pour les incompatibilités de types
- La prise en charge de l’autocomplétion et d’IntelliSense
- La protection contre les erreurs d’exécution
Options d’implémentation de dictionnaire en TypeScript
Utilisation des signatures d’index
L’approche la plus directe utilise des objets JavaScript avec des signatures d’index 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'
Cette approche est simple mais devient encombrante lorsqu’elle est réutilisée dans votre code.
Création d’un type de dictionnaire réutilisable
Pour une meilleure réutilisabilité, définissez un type de dictionnaire générique :
// 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"";
Utilisation du type utilitaire Record
Le type utilitaire intégré Record
de TypeScript offre une syntaxe plus claire :
// 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;
Le type Record
est particulièrement utile lorsque vous devez imposer un ensemble spécifique de clés à l’aide de types littéraux de chaîne.
Utilisation de Map de JavaScript avec TypeScript
Pour des fonctionnalités plus avancées, utilisez l’objet Map
de JavaScript avec des génériques 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
Comparaison des implémentations de dictionnaire
Fonctionnalité Objet avec signature d’index Type Record Map Types de clés String, number, symbol Tout type Tout type Performance Rapide pour petits ensembles de données Rapide pour petits ensembles de données Meilleure pour ajouts/suppressions fréquents Utilisation mémoire Plus faible Plus faible Plus élevée Préservation de l’ordre Non Non Oui (ordre d’insertion) Itération Nécessite Object.entries()
Nécessite Object.entries()
Itérateurs intégrés Méthodes spéciales Non Non has()
, delete()
, clear()
Risque de pollution du prototype Oui Oui Non
Opérations courantes sur les dictionnaires
Vérifier si une clé existe
// 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
Ajouter et mettre à jour des valeurs
// 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
Supprimer des clés
// Using index signature
delete users[""u1""];
// Using Map
userMap.delete(""u1"");
Parcourir les dictionnaires
// 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}`);
});
Techniques avancées de sécurité des types
Contraindre les types de clés
// 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
Dictionnaires en lecture seule
// 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
Dictionnaires partiels
// 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
Utilisation des types mappés pour des dictionnaires avancés
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
};
Considérations de performance
Lors du choix d’une implémentation de dictionnaire, considérez :
- Taille des données : Pour les petits ensembles de données avec des clés de type chaîne, les dictionnaires basés sur des objets sont efficaces
- Modifications fréquentes :
Map
est plus performant pour les ajouts et suppressions fréquents - Utilisation de la mémoire : Les dictionnaires basés sur des objets utilisent moins de mémoire que
Map
- Types de clés : Si vous avez besoin de clés non-chaînes, utilisez
Map
Résultats de benchmark pour 10 000 opérations :
Object access: ~25ms
Map access: ~45ms
Object insertion: ~30ms
Map insertion: ~50ms
Object deletion: ~20ms
Map deletion: ~15ms
Cas d’utilisation réels
Gestion de configuration
type AppConfig = Record<string, string | number | boolean>;
const config: AppConfig = {
apiUrl: ""https://api.example.com"",
timeout: 5000,
enableCache: true
};
Implémentation de cache
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);
Gestion d’état dans 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;
}
}
Stratégies de gestion des erreurs
Gestion des clés manquantes
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""
Guards de type pour les valeurs de dictionnaire
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}`);
}
}
}
Conclusion
Les dictionnaires TypeScript offrent des moyens puissants de gérer des données clé-valeur avec une sécurité de type complète. En choisissant la bonne approche d’implémentation basée sur vos besoins spécifiques, vous pouvez construire des applications plus robustes avec moins d’erreurs d’exécution. Que vous ayez besoin d’objets simples avec des clés de type chaîne ou de dictionnaires complexes avec des fonctionnalités avancées, le système de types de TypeScript garantit que vos opérations de dictionnaire restent type-safe dans tout votre code.
FAQ
Utilisez Map lorsque vous avez besoin de clés non-chaînes, d'une préservation garantie de l'ordre des clés, d'ajouts et de suppressions fréquents, ou d'une protection contre la pollution du prototype. Les Maps fournissent des méthodes intégrées comme has(), delete() et clear() qui rendent certaines opérations plus pratiques
Utilisez des types littéraux de chaîne avec le type utilitaire 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`nnCela garantit que TypeScript générera une erreur si vous essayez d'utiliser des clés qui ne sont pas dans l'union AllowedKeys
Pas avec des dictionnaires basés sur des objets, mais vous le pouvez avec Map :nn`typescriptnconst userMap = new Map<{id: number}, string>();nconst key1 = {id: 1};nconst key2 = {id: 1}; // Référence d'objet différentennuserMap.set(key1, 'Alice');nconsole.log(userMap.get(key1)); // 'Alice'nconsole.log(userMap.get(key2)); // undefined (référence d'objet différente)n`nnNotez que Map utilise l'égalité de référence pour les clés d'objet
Convertir un objet en Map :nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nnConvertir une Map en objet :nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n`
Utilisez le chaînage optionnel et la coalescence des nuls :nn`typescriptnconst users: Record<string, {name: string} | undefined> = {n '1': {name: 'Alice'}n};nn// Accès sécurisé avec valeur par défautnconst userName = users['2']?.name ?? 'Unknown';nconsole.log(userName); // 'Unknown'n`nnCela évite les erreurs d'exécution lors de l'accès aux propriétés de valeurs potentiellement undefined