TypeScript e Web Storage
Questo articolo spiega TypeScript e il Web Storage.
Spieghiamo TypeScript e il Web Storage, con esempi pratici.
YouTube Video
TypeScript e Web Storage
Il Web Storage del browser è un archivio chiave/valore per stringhe. È leggero e offre un'API sincrona, ma tieni presente che può memorizzare solo stringhe e che devi gestire le eccezioni come il superamento della quota di archiviazione. In combinazione con TypeScript, puoi aggiungere sicurezza dei tipi, serializzazione/deserializzazione sicura, gestione centralizzata delle chiavi e scadenza e versioning, ottenendo un design pronto per la produzione.
localStorage e sessionStorage
localStorage è un archivio persistente che rimane dopo la chiusura del browser, mentre sessionStorage è un archivio di sessione per scheda/finestra che viene cancellato quando la scheda viene chiusa. Entrambi memorizzano valori come coppie chiave-valore (stringhe).
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"
- Questo codice è un esempio per salvare e recuperare stringhe. Poiché il
Web Storagepuò memorizzare solo stringhe, gli oggetti devono essere convertiti in JSON per essere memorizzati.
Esempio con parsing JSON
Per memorizzare e ripristinare oggetti in Web Storage, usa 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" }
- Questo codice è un esempio minimo funzionante. Nelle applicazioni reali, devi anche tenere conto di eccezioni come errori di parsing ed esaurimento della quota di archiviazione.
Esempio di gestione delle eccezioni per il parsing JSON
Qui forniamo un wrapper per rendere sicure le letture e le scritture e gestire i fallimenti di JSON.parse e le eccezioni di 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}- Puoi riutilizzare questa utility in tutta l'app. Incapsula ulteriormente quando vuoi aggiungere
try/catch.
Esempio di salvataggio con TTL (scadenza)
Poiché il Web Storage non ha un TTL, gestiscilo aggiungendo un expiresAt al valore.
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}- Il TTL è efficace per cache e salvataggio automatico (bozze) e può ridurre le incoerenze.
Esempio di gestione centralizzata delle chiavi e di namespacing (evitare collisioni)
Standardizzare le chiavi come prefisso + versione + nome riduce le collisioni e semplifica le migrazioni.
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};- Includere un namespace nella chiave rende più facile cambiare versione o eseguire la pulizia in seguito.
Esempio di superamento della quota (QuotaExceededError) e strategie di fallback
Considera che può verificarsi un QuotaExceededError durante l'esecuzione di setItem e progetta una strategia di fallback per quando il salvataggio dei dati fallisce. Ad esempio, quando superi la capacità di archiviazione, puoi eliminare i dati vecchi oppure ripiegare su sessionStorage o su una cache in memoria per mantenere la stabilità complessiva dell'app.
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");- A seconda della destinazione di fallback, la persistenza dei dati potrebbe non essere garantita. Pertanto, scegli una destinazione di archiviazione appropriata in base al tuo caso d'uso. Ad esempio, in modalità di navigazione privata o con limiti di archiviazione, puoi mantenere la funzionalità usando temporaneamente la memoria o
sessionStorage.
Esempio di sincronizzazione tra schede (evento storage) e notifiche nella stessa scheda
Usando window.addEventListener('storage', …), puoi rilevare le modifiche allo storage avvenute in altre schede. Tuttavia, questo evento non si attiva all'interno della stessa scheda. Pertanto, per le notifiche di modifica nella stessa scheda, pubblica eventi personalizzati usando 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});- Con questo codice, puoi sincronizzare le modifiche allo storage sia tra altre schede sia nella scheda corrente.
Registro type-safe (tipizzazione rigorosa per chiave)
Definisci in TypeScript una mappa da chiave a tipo per prevenire errori durante il salvataggio e il recupero.
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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}
Serializzazione/reviver per tipi complessi (Date/Map, ecc.)
Per ripristinare correttamente oggetti come Date e Map, sfrutta il replacer e il reviver di 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}- Usando questo metodo, gli oggetti
Datevengono ripristinati correttamente comeDate. Analogamente, ancheMapeSetpossono essere contrassegnati e ripristinati.
Esempio di versionamento e strategie di migrazione
Se in futuro potresti cambiare il formato di storage, includi una versione nel payload e prepara le migrazioni.
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}- Accumulando piccole migrazioni, puoi mantenere la compatibilità all'indietro.
Esempio di gestione in SSR (server-side rendering)
In ambienti senza window, un riferimento diretto a localStorage causerà un crash, quindi usa controlli dell'ambiente.
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");- Per codice che deve supportare SSR, ricorda sempre di verificare
typeof window.
Suggerimenti pratici
- Limita la frequenza delle scritture con debounce/throttle (per mitigare i picchi dovuti alle azioni dell'interfaccia utente).
- Gestisci le chiavi con namespace + versione, ad es.
app:v1:.... - Per regola, non archiviare informazioni sensibili (access token, ecc.). Se proprio devi, considera durate brevi più validazione lato server o WebCrypto.
- La capacità dipende dal browser (pochi MB), quindi archivia i dati grandi in IndexedDB.
- Usa
CustomEventper le notifiche nella stessa scheda estorageper la comunicazione tra schede. - In SSR, verifica sempre
typeof window.
Classe 'store' type-safe consolidata
Vediamo un esempio di implementazione di una classe generica che integra gli elementi trattati finora, inclusi namespace, sicurezza dei tipi, scadenza (TTL) e gestione delle eccezioni. Nei prodotti reali, considera di aggiungere test, logging, eliminazione dei dati vecchi basata su LRU, crittografia e altro.
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}- Questo codice mostra la configurazione e le definizioni di tipo per costruire uno storage key-value sicuro rispetto ai tipi e ricco di funzionalità.
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; }- Questo codice costituisce il nucleo della classe
TypedStorageche fornisce uno storage chiave-valore con sicurezza dei tipi. Gestisce le chiavi consentite e i rispettivi tipi in base alregistrye genera chiavi di storage con prefisso. Inoltre, usalocalStoragee un fallback in memoria e consente di impostare un nome di evento per le notifiche di modifica nella stessa scheda.
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 }- Il metodo
getrecupera il valore per la chiave specificata in modo sicuro rispetto ai tipi e può opzionalmente gestire valori con TTL (scadenza).
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 }- Il metodo
setsalva un valore sotto la chiave specificata e restituisce un booleano che indica se l'operazione è riuscita.
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 }- Il metodo
setWithTTLsalva un valore con un TTL (scadenza).
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 }- Il metodo
removeelimina il valore per la chiave specificata sia dallo storage sia dal fallback.
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}- Il metodo
clearelimina tutti i dati memorizzati sia nello storage sia nel fallback.
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
-
Questo codice è un esempio che utilizza
TypedStorage, il quale salva e recupera valori come"theme"e"draft"in uno storage chiave-valore sicuro rispetto ai tipi e supporta anche TTL e un fallback. Configura notifiche nella stessa scheda e un fallback in memoria per eseguire le operazioni di storage in modo sicuro. -
La classe
TypedStorageè un punto di partenza pratico. Implementa strategie LRU, crittografia, compressione e fallback a IndexedDB secondo necessità.
Riepilogo
Quando usi il Web Storage con TypeScript, tieni sempre a mente quattro punti—sicurezza dei tipi, resilienza alle eccezioni, sicurezza e sincronizzazione (schede multiple)—per un design robusto. I wrapper e le utility che abbiamo visto finora ne sono un esempio. Puoi anche migrare ad altri storage del browser come IndexedDB secondo necessità.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.