Back

Dictionnaire TypeScript : Guide complet pour des objets type-safe

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 et Map
  • Choisissez l’implémentation selon vos besoins : signatures d’index pour les dictionnaires simples, Record pour des ensembles de clés spécifiques, et Map 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 :

  1. 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
  2. Modifications fréquentes : Map est plus performant pour les ajouts et suppressions fréquents
  3. Utilisation de la mémoire : Les dictionnaires basés sur des objets utilisent moins de mémoire que Map
  4. 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

Listen to your bugs 🧘, with OpenReplay

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