TypeScript y el almacenamiento web
Este artículo explica TypeScript y el almacenamiento web.
Explicamos TypeScript y el almacenamiento web, con ejemplos prácticos.
YouTube Video
TypeScript y el almacenamiento web
El Web Storage del navegador es un almacenamiento clave/valor para cadenas de texto. Es ligero y ofrece una API sincrónica, pero ten en cuenta que solo almacena cadenas y que debes manejar excepciones, como superar la cuota de almacenamiento. Al combinarlo con TypeScript, puedes añadir seguridad de tipos, serialización/deserialización seguras, gestión centralizada de claves y expiración y versionado, obteniendo un diseño listo para producción.
localStorage y sessionStorage
localStorage es un almacenamiento persistente que permanece tras cerrar el navegador, mientras que sessionStorage es un almacenamiento de sesión por pestaña/ventana que se borra cuando se cierra la pestaña. Ambos almacenan valores como pares clave-valor (cadenas).
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"
- Este código es un ejemplo para guardar y recuperar cadenas. Como
Web Storagesolo puede almacenar cadenas, los objetos deben convertirse a JSON para poder almacenarse.
Ejemplo usando análisis de JSON
Para almacenar y restaurar objetos en 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" }
- Este código es un ejemplo mínimo funcional. En aplicaciones reales, también debes tener en cuenta excepciones como errores de parseo y agotar la cuota de almacenamiento.
Ejemplo de manejo de excepciones para el análisis de JSON
Aquí proporcionamos un envoltorio para hacer seguras las lecturas y escrituras y manejar fallos de JSON.parse y excepciones 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}- Puedes reutilizar esta utilidad en toda la aplicación. Crea un envoltorio adicional cuando quieras añadir
try/catch.
Ejemplo de guardado con TTL (expiración)
Como Web Storage no tiene TTL, gestiona esto añadiendo un expiresAt al valor.
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}- El TTL es eficaz para cachés y autoguardado (borradores) y puede reducir inconsistencias.
Ejemplo de gestión centralizada de claves y espacios de nombres (evitar colisiones)
Estandarizar las claves como prefijo + versión + nombre reduce las colisiones y simplifica las migraciones.
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};- Incluir un espacio de nombres en la clave facilita cambiar de versión o realizar limpieza más adelante.
Ejemplo de cuota excedida (QuotaExceededError) y estrategias de respaldo
Ten en cuenta que puede ocurrir un QuotaExceededError al ejecutar setItem, y diseña una estrategia de respaldo para cuando falle el guardado de datos. Por ejemplo, cuando superes la capacidad de almacenamiento, puedes eliminar datos antiguos o recurrir a sessionStorage o a una caché en memoria para mantener la estabilidad general de la aplicación.
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");- Según el destino de respaldo, la persistencia de los datos puede no estar garantizada. Por lo tanto, elige un destino de almacenamiento apropiado según tu caso de uso. Por ejemplo, en modo de navegación privada o bajo límites de almacenamiento, puedes mantener la funcionalidad usando temporalmente la memoria o
sessionStorage.
Ejemplo de sincronización entre pestañas (evento storage) y notificaciones en la misma pestaña
Usando window.addEventListener('storage', …), puedes detectar cambios de almacenamiento que ocurrieron en otras pestañas. Sin embargo, este evento no se dispara dentro de la misma pestaña. Por lo tanto, para notificaciones de cambios dentro de la misma pestaña, publica tus propios eventos 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 este código, puedes sincronizar los cambios de almacenamiento tanto en otras pestañas como en la pestaña actual.
Registro con tipos seguros (tipado estricto por clave)
Define un mapa de clave a tipo en TypeScript para evitar errores durante el guardado y la recuperación.
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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}
Serialización/reviver para tipos complejos (Date/Map, etc.)
Para restaurar correctamente objetos como Date y Map, aprovecha el replacer y el 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}- Usando este método, los objetos
Datese restauran correctamente comoDate. De manera similar,MapySettambién pueden marcarse y restaurarse.
Ejemplo de versionado y estrategias de migración
Si pudieras cambiar el formato de almacenamiento en el futuro, incluye una versión en la carga útil y prepara migraciones.
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}- Apilando pequeñas migraciones, puedes mantener la compatibilidad hacia atrás.
Ejemplo de manejo en SSR (renderizado del lado del servidor)
En entornos sin window, referenciar directamente localStorage provocará un fallo, así que usa comprobaciones de entorno.
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");- Para código que deba soportar SSR, recuerda siempre comprobar
typeof window.
Consejos prácticos
- Limita la frecuencia de escritura con debounce/throttle (para mitigar ráfagas originadas por acciones de la UI).
- Gestiona las claves con un espacio de nombres + versión, p. ej.,
app:v1:.... - Como regla, no almacenes información sensible (tokens de acceso, etc.). Si es imprescindible, considera vidas útiles cortas más validación en el servidor o WebCrypto.
- La capacidad depende del navegador (unos pocos MB), así que almacena datos grandes en IndexedDB.
- Usa
CustomEventpara notificaciones en la misma pestaña ystoragepara entre pestañas. - En SSR, comprueba siempre
typeof window.
Clase consolidada de 'almacén con tipos seguros'
Veamos un ejemplo de implementación de una clase genérica que integra los elementos que hemos visto hasta ahora, incluidos los espacios de nombres, la seguridad de tipos, la expiración (TTL) y el manejo de excepciones. En productos reales, considera añadir pruebas, registro (logging), eliminación de datos antiguos basada en LRU, cifrado y más.
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}- Este código muestra la configuración y las definiciones de tipos para crear un almacenamiento clave-valor con seguridad de tipos y rico en funcionalidades.
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; }- Este código es el núcleo de la clase
TypedStorageque proporciona un almacenamiento clave-valor con tipado seguro. Gestiona las claves permitidas y sus tipos en función delregistryy genera claves de almacenamiento con prefijo. Además, utilizalocalStoragey un respaldo en memoria, y te permite establecer un nombre de evento para las notificaciones de cambios en la misma pestaña.
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 }- El método
getrecupera el valor para la clave especificada de forma segura respecto a los tipos y puede, opcionalmente, manejar valores con TTL (expiración).
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 }- El método
setguarda un valor bajo la clave especificada y devuelve un booleano que indica si tuvo éxito.
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 }- El método
setWithTTLguarda un valor con un TTL (expiración).
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 }- El método
removeelimina el valor de la clave especificada tanto del almacenamiento como del respaldo.
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}- El método
clearelimina todos los datos almacenados tanto en el almacenamiento como en el respaldo.
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
-
Este código es un ejemplo que usa
TypedStorage, el cual guarda y recupera valores como"theme"y"draft"en un almacenamiento clave-valor con tipado seguro, y también admite TTL y un respaldo. Configura notificaciones dentro de la misma pestaña y un respaldo en memoria para realizar operaciones de almacenamiento de forma segura. -
La clase
TypedStoragees un punto de partida práctico. Implementa estrategias LRU, cifrado, compresión y respaldos a IndexedDB según sea necesario.
Resumen
Al usar Web Storage con TypeScript, ten siempre en mente cuatro puntos: seguridad de tipos, resiliencia ante excepciones, seguridad y sincronización (múltiples pestañas) para un diseño robusto. Los envoltorios y utilidades que hemos visto hasta ahora son ejemplos de ello. También puedes migrar a otros almacenamientos del navegador como IndexedDB según sea necesario.
Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.