Français

Libérez la puissance des structures de données immuables en TypeScript avec les types readonly. Apprenez à créer des applications plus prévisibles, maintenables et robustes en empêchant les mutations de données non intentionnelles.

Types Readonly TypeScript : Maîtriser les structures de données immuables

Dans le paysage en constante évolution du développement logiciel, la recherche d'un code robuste, prévisible et maintenable est un effort constant. TypeScript, avec son système de typage fort, fournit des outils puissants pour atteindre ces objectifs. Parmi ces outils, les types readonly se distinguent comme un mécanisme crucial pour appliquer l'immuabilité, une pierre angulaire de la programmation fonctionnelle et une clé pour construire des applications plus fiables.

Qu'est-ce que l'immuabilité et pourquoi est-ce important ?

L'immuabilité, à la base, signifie qu'une fois qu'un objet est créé, son état ne peut pas être modifié. Ce concept simple a des implications profondes sur la qualité et la maintenabilité du code.

Les types Readonly en TypeScript : votre arsenal pour l'immuabilité

TypeScript offre plusieurs façons d'appliquer l'immuabilité en utilisant le mot-clé readonly. Explorons les différentes techniques et comment elles peuvent être appliquées en pratique.

1. Propriétés Readonly sur les interfaces et les types

La manière la plus directe de déclarer une propriété en lecture seule est d'utiliser le mot-clé readonly directement dans une définition d'interface ou de type.


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // Erreur : Impossible d'assigner à 'id' car c'est une propriété en lecture seule.
person.name = "Bob"; // Ceci est autorisé

Dans cet exemple, la propriété id est déclarée comme readonly. TypeScript empêchera toute tentative de la modifier après la création de l'objet. Les propriétés name et age, sans le modificateur readonly, peuvent être modifiées librement.

2. Le type utilitaire Readonly

TypeScript propose un type utilitaire puissant appelé Readonly<T>. Ce type générique prend un type existant T et le transforme en rendant toutes ses propriétés readonly.


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // Erreur : Impossible d'assigner à 'x' car c'est une propriété en lecture seule.

Le type Readonly<Point> crée un nouveau type où x et y sont tous deux readonly. C'est un moyen pratique de rendre rapidement un type existant immuable.

3. Tableaux Readonly (ReadonlyArray<T>) et readonly T[]

Les tableaux en JavaScript sont intrinsèquement muables. TypeScript fournit un moyen de créer des tableaux en lecture seule en utilisant le type ReadonlyArray<T> ou le raccourci readonly T[]. Cela empêche la modification du contenu du tableau.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // Erreur : La propriété 'push' n'existe pas sur le type 'readonly number[]'.
// numbers[0] = 10; // Erreur : La signature d'index dans le type 'readonly number[]' ne permet que la lecture.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // Équivalent à ReadonlyArray
// moreNumbers.push(11); // Erreur : La propriété 'push' n'existe pas sur le type 'readonly number[]'.

Tenter d'utiliser des méthodes qui modifient le tableau, telles que push, pop, splice, ou d'assigner directement à un index, entraînera une erreur TypeScript.

4. const vs readonly : Comprendre la différence

Il est important de faire la distinction entre const et readonly. const empêche la réassignation de la variable elle-même, tandis que readonly empêche la modification des propriétés de l'objet. Ils servent des objectifs différents et peuvent être utilisés ensemble pour une immuabilité maximale.


const immutableNumber = 42;
// immutableNumber = 43; // Erreur : Impossible de réassigner à la variable const 'immutableNumber'.

const mutableObject = { value: 10 };
mutableObject.value = 20; // Ceci est autorisé car l'*objet* n'est pas const, seulement la variable.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // Erreur : Impossible d'assigner à 'value' car c'est une propriété en lecture seule.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // Erreur : Impossible de réassigner à la variable const 'constReadonlyObject'.
// constReadonlyObject.value = 60; // Erreur : Impossible d'assigner à 'value' car c'est une propriété en lecture seule.

Comme démontré ci-dessus, const garantit que la variable pointe toujours vers le même objet en mémoire, tandis que readonly garantit que l'état interne de l'objet reste inchangé.

Exemples pratiques : Appliquer les types Readonly dans des scénarios réels

Explorons quelques exemples pratiques de la manière dont les types readonly peuvent être utilisés pour améliorer la qualité et la maintenabilité du code dans divers scénarios.

1. Gérer les données de configuration

Les données de configuration sont souvent chargées une seule fois au démarrage de l'application et ne devraient pas être modifiées pendant l'exécution. L'utilisation de types readonly garantit que ces données restent cohérentes et empêche les modifications accidentelles.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... utiliser config.timeout et config.apiUrl en toute sécurité, sachant qu'ils ne changeront pas
}

fetchData("/data", config);

2. Implémenter une gestion d'état de type Redux

Dans les bibliothèques de gestion d'état comme Redux, l'immuabilité est un principe fondamental. Les types readonly peuvent être utilisés pour garantir que l'état reste immuable et que les réducteurs (reducers) retournent uniquement de nouveaux objets d'état au lieu de modifier les existants.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // Retourner un nouvel objet d'état
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // Retourner un nouvel objet d'état avec les items mis à jour
    default:
      return state;
  }
}

3. Travailler avec les réponses d'API

Lors de la récupération de données depuis une API, il est souvent souhaitable de traiter les données de la réponse comme immuables, surtout si vous les utilisez pour le rendu de composants d'interface utilisateur. Les types readonly peuvent aider à prévenir les mutations accidentelles des données de l'API.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // Erreur : Impossible d'assigner à 'completed' car c'est une propriété en lecture seule.
});

4. Modéliser des données géographiques (Exemple international)

Imaginons la représentation de coordonnées géographiques. Une fois qu'une coordonnée est définie, elle devrait idéalement rester constante. Cela garantit l'intégrité des données, en particulier lorsqu'il s'agit d'applications sensibles comme les systèmes de cartographie ou de navigation qui opèrent dans différentes régions géographiques (par exemple, les coordonnées GPS pour un service de livraison couvrant l'Amérique du Nord, l'Europe et l'Asie).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // Imaginez un calcul complexe utilisant la latitude et la longitude
 // Retour d'une valeur fictive pour la simplicité
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Distance entre Tokyo et New York (fictive) :", distance);

// tokyoCoordinates.latitude = 36.0; // Erreur : Impossible d'assigner à 'latitude' car c'est une propriété en lecture seule.

Types profondément Readonly : Gérer les objets imbriqués

Le type utilitaire Readonly<T> ne rend readonly que les propriétés directes d'un objet. Si un objet contient des objets ou des tableaux imbriqués, ces structures imbriquées restent muables. Pour atteindre une véritable immuabilité profonde, vous devez appliquer récursivement Readonly<T> à toutes les propriétés imbriquées.

Voici un exemple de comment créer un type profondément readonly :


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // Erreur
// company.address.city = "New City"; // Erreur
// company.employees.push("Charlie"); // Erreur

Ce type DeepReadonly<T> applique récursivement Readonly<T> à toutes les propriétés imbriquées, garantissant que toute la structure de l'objet est immuable.

Considérations et compromis

Bien que l'immuabilité offre des avantages significatifs, il est important d'être conscient des compromis potentiels.

Bibliothèques pour les structures de données immuables

Plusieurs bibliothèques peuvent simplifier le travail avec des structures de données immuables en TypeScript :

Meilleures pratiques pour l'utilisation des types Readonly

Pour exploiter efficacement les types readonly dans vos projets TypeScript, suivez ces meilleures pratiques :

Conclusion : Adopter l'immuabilité avec les types Readonly de TypeScript

Les types readonly de TypeScript sont un outil puissant pour construire des applications plus prévisibles, maintenables et robustes. En adoptant l'immuabilité, vous pouvez réduire le risque de bogues, simplifier le débogage et améliorer la qualité globale de votre code. Bien qu'il y ait quelques compromis à considérer, les avantages de l'immuabilité l'emportent souvent sur les coûts, en particulier dans les projets complexes et de longue durée. Alors que vous poursuivez votre parcours avec TypeScript, faites des types readonly une partie centrale de votre flux de travail de développement pour libérer tout le potentiel de l'immuabilité et construire des logiciels véritablement fiables.

Types Readonly TypeScript : Maîtriser les structures de données immuables pour des applications robustes | MLOG