TypeScript et le stockage Web
Cet article explique TypeScript et le stockage Web.
Nous expliquons TypeScript et le stockage Web, avec des exemples pratiques.
YouTube Video
TypeScript et le stockage Web
Le Web Storage du navigateur est un stockage clé/valeur pour des chaînes de caractères. Il est léger avec une API synchrone, mais gardez à l'esprit qu'il ne peut stocker que des chaînes de caractères uniquement, et vous devez gérer les exceptions comme le dépassement du quota de stockage. Combiné à TypeScript, vous pouvez ajouter une sécurité de typage, une sérialisation/désérialisation sûre, une gestion centralisée des clés ainsi que l'expiration et le versionnage, aboutissant à une conception prête pour la production.
localStorage et sessionStorage
localStorage est un stockage persistant qui demeure après la fermeture du navigateur, tandis que sessionStorage est un stockage de session par onglet/fenêtre qui est effacé à la fermeture de l’onglet. Les deux stockent des valeurs sous forme de paires clé-valeur (chaînes).
1// Simple usage example: store and retrieve a string
2// This example uses localStorage to persist a username.
3localStorage.setItem('username', 'alice');
4console.log('saved username:', localStorage.getItem('username')); // "alice"
5
6// session storage example
7sessionStorage.setItem('session-id', 'xyz-123');
8console.log('session id:', sessionStorage.getItem('session-id')); // "xyz-123"
- Ce code est un exemple pour enregistrer et récupérer des chaînes de caractères. Comme le
Web Storagene peut stocker que des chaînes, les objets doivent être convertis en JSON pour être stockés.
Exemple utilisant l’analyse JSON
Pour stocker et restaurer des objets dans le Web Storage, utilisez JSON.stringify / JSON.parse.
1// Simple write & read with JSON
2// Store a small object as JSON and read it back.
3const user = { id: 1, name: "Alice" };
4localStorage.setItem("app:user", JSON.stringify(user));
5
6const raw = localStorage.getItem("app:user");
7const parsed = raw ? (JSON.parse(raw) as { id: number; name: string }) : null;
8
9console.log("user:", parsed); // user: { id: 1, name: "Alice" }
- Ce code est un exemple minimal fonctionnel. Dans des applications réelles, vous devez également tenir compte des exceptions telles que les échecs d'analyse et l'épuisement du quota de stockage.
Exemple de gestion d’exceptions lors de l’analyse JSON
Ici, nous fournissons un wrapper pour sécuriser les lectures et écritures et gérer les échecs de JSON.parse ainsi que les exceptions de setItem.
1// Safe JSON helpers
2// These helpers handle parse errors and memory/storage issues separately.
3export function safeParseJson<T>(raw: string | null): T | null {
4 if (raw == null) return null;
5
6 try {
7 return JSON.parse(raw) as T;
8 } catch (error: any) {
9 if (error instanceof SyntaxError) {
10 console.error("JSON parse error:", error.message);
11 return null;
12 }
13
14 console.error("Unexpected JSON error:", error);
15 return null;
16 }
17}
18
19// Safe setter for JSON values
20export function setJson(storage: Storage, key: string, value: unknown): void {
21 try {
22 const json = JSON.stringify(value);
23 storage.setItem(key, json);
24 } catch (error: any) {
25 if (error?.name === "QuotaExceededError") {
26 console.error("Storage quota exceeded while saving JSON:", error.message);
27 } else if (error instanceof TypeError) {
28 console.error("JSON serialization failed:", error.message);
29 } else {
30 console.error("Unexpected error while setting JSON:", error);
31 }
32 }
33}
34
35// Safe getter for JSON values
36export function getJson<T>(storage: Storage, key: string, fallback?: T): T | null {
37 const parsed = safeParseJson<T>(storage.getItem(key));
38 return parsed ?? (fallback ?? null);
39}- Vous pouvez réutiliser cet utilitaire dans toute l’application. Encapsulez davantage lorsque vous souhaitez ajouter des
try/catch.
Exemple d’enregistrement avec TTL (expiration)
Comme le Web Storage n’a pas de TTL en soi, gérez-le en ajoutant un expiresAt à la valeur.
1// TTL wrapper
2// Store { value, expiresAt } and automatically expire items on read.
3type WithTTL<T> = { value: T; expiresAt: number | null };
4
5export function setWithTTL<T>(storage: Storage, key: string, value: T, ttlMs?: number) {
6 const payload: WithTTL<T> = { value, expiresAt: ttlMs ? Date.now() + ttlMs : null };
7 setJson(storage, key, payload);
8}
9
10export function getWithTTL<T>(storage: Storage, key: string): T | null {
11 const payload = getJson<WithTTL<T>>(storage, key);
12 if (!payload) return null;
13 if (payload.expiresAt && Date.now() > payload.expiresAt) {
14 storage.removeItem(key);
15 return null;
16 }
17 return payload.value;
18}- Le TTL est efficace pour le cache et l’enregistrement automatique (brouillons) et peut réduire les incohérences.
Exemple de gestion centralisée des clés et d’espaces de noms (afin d’éviter les collisions)
Standardiser les clés sous la forme préfixe + version + nom réduit les collisions et simplifie les migrations.
1// Key factory with namespacing and versioning
2// Create namespaced keys like "myapp:v1:theme" to avoid collisions.
3const APP = "myapp";
4const V = "v1";
5
6const ns = (k: string) => `${APP}:${V}:${k}`;
7
8const Keys = {
9 theme: ns("theme"),
10 user: ns("user"),
11 cart: ns("cart"),
12 draft: ns("draft"),
13};- Inclure un espace de noms dans la clé facilite le changement de version ou le nettoyage ultérieur.
Exemple de dépassement de quota (QuotaExceededError) et de stratégies de repli
Tenez compte du fait qu'un QuotaExceededError peut survenir lors de l'exécution de setItem, et concevez une stratégie de repli pour les cas où l'enregistrement des données échoue. Par exemple, lorsque vous dépassez la capacité de stockage, vous pouvez supprimer les anciennes données ou basculer vers sessionStorage ou un cache en mémoire pour maintenir la stabilité globale de l'application.
1// Quota-safe set with fallback to in-memory storage
2// Return true if stored, false otherwise.
3export function trySetJson(storage: Storage, key: string, value: unknown, fallback?: Map<string, string>): boolean {
4 try {
5 storage.setItem(key, JSON.stringify(value));
6 return true;
7 } catch (err) {
8 console.warn("Failed to set item:", key, err);
9 if (fallback) {
10 try {
11 fallback.set(key, JSON.stringify(value));
12 return true;
13 } catch {
14 return false;
15 }
16 }
17 return false;
18 }
19}
20
21// Example fallback usage
22const inMemoryFallback = new Map<string, string>();
23const ok = trySetJson(localStorage, Keys.cart, { items: [] }, inMemoryFallback);
24if (!ok) console.log("Saved to fallback map instead");- Selon la destination de repli, la persistance des données peut ne pas être garantie. Choisissez donc une destination de stockage appropriée en fonction de votre cas d'utilisation. Par exemple, en mode de navigation privée ou sous des limites de stockage, vous pouvez maintenir le fonctionnement en utilisant temporairement la mémoire ou
sessionStorage.
Exemple de synchronisation entre onglets (événement storage) et de notifications dans le même onglet
Avec window.addEventListener('storage', …), vous pouvez détecter les modifications de stockage survenues dans d'autres onglets. Cependant, cet événement ne se déclenche pas dans le même onglet. Par conséquent, pour les notifications de changement dans le même onglet, publiez vos propres événements à l'aide de CustomEvent.
1// Cross-tab and same-tab notification helpers
2// storage event fires on other tabs; use CustomEvent for same-tab listeners.
3const SAME_TAB_EVENT = "storage:changed";
4
5function notifyChanged(key: string) {
6 window.dispatchEvent(new CustomEvent(SAME_TAB_EVENT, { detail: { key } }));
7}
8
9function setJsonWithNotify(storage: Storage, key: string, value: unknown) {
10 setJson(storage, key, value);
11 notifyChanged(key);
12}
13
14// Listeners
15window.addEventListener("storage", (e) => {
16 if (e.key === Keys.theme) {
17 const theme = safeParseJson<string>(e.newValue);
18 console.log("Theme changed in another tab:", theme);
19 }
20});
21
22window.addEventListener(SAME_TAB_EVENT, (e: Event) => {
23 const detail = (e as CustomEvent).detail as { key: string };
24 console.log("Changed in this tab:", detail.key);
25});- Avec ce code, vous pouvez synchroniser les modifications de stockage à la fois dans d'autres onglets et dans l'onglet actuel.
Registre à typage sûr (typage strict par clé)
Définissez une correspondance clé→type en TypeScript pour éviter les erreurs lors de l'enregistrement et de la récupération.
1// Typed registry that enforces types per key
2// Registry maps keys to their allowed types.
3type Registry = {
4 [k in typeof Keys.theme]: "light" | "dark";
5} & {
6 [k in typeof Keys.user]: { id: number; name: string };
7};
8
9type KeyOf<R> = Extract<keyof R, string>;
10
11export const TypedStore = {
12 get<K extends KeyOf<Registry>>(key: K, storage: Storage = localStorage): Registry[K] | null {
13 return getJson<Registry[K]>(storage, key);
14 },
15 set<K extends KeyOf<Registry>>(key: K, value: Registry[K], storage: Storage = localStorage): void {
16 setJson(storage, key, value);
17 },
18 remove<K extends KeyOf<Registry>>(key: K, storage: Storage = localStorage): void {
19 storage.removeItem(key);
20 },
21};- {^ i18n_speak 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}
Sérialisation/reviver pour les types complexes (Date/Map, etc.)
Pour restaurer correctement des objets tels que Date et Map, exploitez le replacer et le reviver de JSON.stringify.
1// Serializing Dates with replacer and reviver
2// Custom replacer marks Date objects for correct revival.
3function replacer(_k: string, v: unknown) {
4 if (v instanceof Date) return { __type: "Date", value: v.toISOString() };
5 return v;
6}
7
8function reviver(_k: string, v: any) {
9 if (v && v.__type === "Date") return new Date(v.value);
10 return v;
11}
12
13function setJsonWithDates(storage: Storage, key: string, value: unknown) {
14 storage.setItem(key, JSON.stringify(value, replacer));
15}
16
17function getJsonWithDates<T>(storage: Storage, key: string): T | null {
18 const raw = storage.getItem(key);
19 if (!raw) return null;
20 try { return JSON.parse(raw, reviver) as T; } catch { return null; }
21}- Avec cette méthode, les objets
Datesont correctement restaurés enDate. De même,MapetSetpeuvent aussi être marqués et restaurés.
Exemple de versionnage et de stratégies de migration
Si vous êtes susceptible de changer le format de stockage à l’avenir, incluez une version dans la charge utile et préparez des migrations.
1// Versioned payload pattern for migrations
2// Keep { v, data } and migrate on read if necessary.
3type VersionedPayload<T> = { v: number; data: T };
4
5function migrateUserV1toV2(u1: { id: number; name: string }) {
6 return { id: u1.id, profile: { displayName: u1.name } };
7}
8
9function readUserAnyVersion(): { id: number; profile: { displayName: string } } | null {
10 const raw = localStorage.getItem(Keys.user);
11 if (!raw) return null;
12 try {
13 const obj = JSON.parse(raw) as VersionedPayload<any>;
14 if (obj.v === 2) {
15 return obj.data;
16 } else if (obj.v === 1) {
17 const migrated = migrateUserV1toV2(obj.data);
18 localStorage.setItem(Keys.user, JSON.stringify({ v: 2, data: migrated }));
19 return migrated;
20 }
21 return null;
22 } catch (err) {
23 console.error("migration parse error", err);
24 return null;
25 }
26}- En enchaînant de petites migrations, vous pouvez maintenir la rétrocompatibilité.
Exemple de prise en charge en SSR (server-side rendering)
Dans les environnements sans window, référencer directement localStorage provoquera un crash ; utilisez donc des gardes d’environnement.
1// Guard for SSR
2// Return a Storage-compatible object or null when not in browser.
3export const isBrowser = (): boolean => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4
5export const safeLocalStorage = (): Storage | null => (isBrowser() ? window.localStorage : null);
6
7// Usage
8const ls = safeLocalStorage();
9if (ls) setJson(ls, Keys.theme, "dark");- Pour le code devant supporter le SSR, pensez toujours à vérifier
typeof window.
Conseils pratiques
- Limitez la fréquence d’écriture avec un debounce/throttle (pour atténuer les rafales dues aux actions UI).
- Gérez les clés avec un espace de noms + version, par ex.
app:v1:.... - Par principe, ne stockez pas d’informations sensibles (jetons d’accès, etc.). Si nécessaire, envisagez des durées de vie courtes plus une validation côté serveur ou WebCrypto.
- La capacité dépend du navigateur (quelques Mo) ; stockez donc les données volumineuses dans IndexedDB.
- Utilisez
CustomEventpour les notifications dans le même onglet etstorageentre onglets. - En SSR, vérifiez toujours
typeof window.
Classe « store à sécurité de type » consolidée
Voyons une implémentation d'exemple d'une classe générique qui intègre les éléments que nous avons abordés jusqu'à présent, notamment les espaces de noms, la sécurité de type, l'expiration (TTL) et la gestion des exceptions. Dans des produits réels, envisagez d'ajouter des tests, de la journalisation, la suppression des anciennes données basée sur LRU, du chiffrement, etc.
1// Comprehensive TypedStorage store integrating many patterns shown above.
2// - type-safe registry per key
3// - prefix (namespace + version)
4// - trySet with fallback
5// - same-tab notify
6// - TTL optional getter/setter
7type Jsonifiable = string | number | boolean | null | Jsonifiable[] | { [k: string]: Jsonifiable };
8
9interface StoreOptions {
10 storage?: Storage | null; // default: auto-detected localStorage or null
11 prefix?: string; // e.g., "myapp:v1"
12 sameTabEvent?: string | null;
13 fallback?: Map<string, string>; // in-memory fallback
14}- Ce code montre la configuration et les définitions de types pour construire un stockage clé-valeur à typage sûr et riche en fonctionnalités.
1export class TypedStorage<Reg extends Record<string, Jsonifiable | object>> {
2 private storage: Storage | null;
3 private prefix: string;
4 private sameTabEvent: string | null;
5 private fallback?: Map<string, string>;
6
7 constructor(private registry: Reg, opts: StoreOptions = {}) {
8 this.storage = opts.storage ?? (typeof window !== "undefined" ? window.localStorage : null);
9 this.prefix = (opts.prefix ?? "app:v1") + ":";
10 this.sameTabEvent = opts.sameTabEvent ?? "storage:changed";
11 this.fallback = opts.fallback;
12 }
13
14 private k(key: keyof Reg & string) { return this.prefix + key; }- Ce code constitue le cœur de la classe
TypedStorage, qui fournit un stockage clé-valeur avec sécurité de type. Il gère les clés autorisées et leurs types en fonction duregistry, et génère des clés de stockage préfixées. En outre, il utiliselocalStorageet un repli en mémoire, et vous permet de définir un nom d'événement pour les notifications de changement dans le même onglet.
1 // Basic get with optional TTL-aware retrieval
2 get<K extends keyof Reg & string>(key: K): Reg[K] | null {
3 const fullKey = this.k(key);
4 try {
5 const raw = this.storage ? this.storage.getItem(fullKey) : this.fallback?.get(fullKey) ?? null;
6 if (!raw) return null;
7 // Check if value is TTL-wrapped
8 const maybe = safeParseJson<{ value: Reg[K]; expiresAt?: number }>(raw);
9 if (maybe && typeof maybe.expiresAt === "number") {
10 if (maybe.expiresAt && Date.now() > maybe.expiresAt) {
11 this.remove(key);
12 return null;
13 }
14 return maybe.value;
15 }
16 return safeParseJson<Reg[K]>(raw);
17 } catch (err) {
18 console.error("TypedStorage.get error", err);
19 return null;
20 }
21 }- La méthode
getrécupère la valeur pour la clé spécifiée de manière sûre au niveau des types et peut, en option, gérer des valeurs avec un TTL (expiration).
1 // Basic set; returns success boolean
2 set<K extends keyof Reg & string>(key: K, value: Reg[K]): boolean {
3 const fullKey = this.k(key);
4 const payload = JSON.stringify(value);
5 try {
6 if (this.storage) this.storage.setItem(fullKey, payload);
7 else this.fallback?.set(fullKey, payload);
8 if (this.sameTabEvent) window.dispatchEvent(new CustomEvent(this.sameTabEvent, { detail: { key: fullKey } }));
9 return true;
10 } catch (err) {
11 console.warn("TypedStorage.set primary failed, trying fallback", err);
12 try {
13 if (this.fallback) {
14 this.fallback.set(fullKey, payload);
15 return true;
16 }
17 return false;
18 } catch (e) {
19 console.error("TypedStorage.set fallback failed", e);
20 return false;
21 }
22 }
23 }- La méthode
setenregistre une valeur sous la clé spécifiée et renvoie un booléen indiquant si l'opération a réussi.
1 // Set with TTL convenience
2 setWithTTL<K extends keyof Reg & string>(key: K, value: Reg[K], ttlMs?: number): boolean {
3 const payload = { value, expiresAt: ttlMs ? Date.now() + ttlMs : null };
4 return this.set(key, payload as unknown as Reg[K]);
5 }- La méthode
setWithTTLenregistre une valeur avec un TTL (expiration).
1 remove<K extends keyof Reg & string>(key: K) {
2 const fullKey = this.k(key);
3 try {
4 if (this.storage) this.storage.removeItem(fullKey);
5 this.fallback?.delete(fullKey);
6 } catch (err) { console.warn("TypedStorage.remove error", err); }
7 }- La méthode
removesupprime la valeur correspondant à la clé spécifiée, à la fois du stockage et du mécanisme de repli.
1 clear() {
2 try {
3 if (this.storage) this.storage.clear();
4 this.fallback?.clear();
5 } catch (err) { console.warn("TypedStorage.clear error", err); }
6 }
7}- La méthode
clearsupprime toutes les données stockées, à la fois dans le stockage et dans le mécanisme de repli.
1// Usage example
2type MyReg = {
3 theme: "light" | "dark";
4 user: { id: number; name: string };
5 draft: string;
6};
7
8const memFallback = new Map<string, string>();
9const store = new TypedStorage<MyReg>({} as MyReg, {
10 prefix: "myapp:v1",
11 sameTabEvent: "storage:changed",
12 fallback: memFallback
13});
14store.set("theme", "dark");
15console.log(store.get("theme")); // "dark"
16store.setWithTTL("draft", "in-progress...", 1000 * 60 * 60); // keep 1 hour
-
Ce code est un exemple utilisant
TypedStorage, qui enregistre et récupère des valeurs comme"theme"et"draft"dans un stockage clé-valeur avec sécurité de type, et prend également en charge le TTL et un mécanisme de repli. Il configure des notifications dans le même onglet et un repli en mémoire pour effectuer les opérations de stockage en toute sécurité. -
La classe
TypedStorageconstitue un point de départ pratique. Implémentez des stratégies LRU, du chiffrement, de la compression et des mécanismes de repli vers IndexedDB selon les besoins.
Résumé
Lorsque vous utilisez le Web Storage avec TypeScript, gardez toujours à l’esprit quatre points — sécurité de typage, résilience aux exceptions, sécurité et synchronisation (plusieurs onglets) — pour un design robuste. Les wrappers et utilitaires vus jusqu’ici en sont des exemples. Vous pouvez également migrer vers d’autres stockages du navigateur comme IndexedDB selon les besoins.
Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.