TypeScript e Web Storage

TypeScript e Web Storage

Este artigo explica TypeScript e Web Storage.

Explicamos TypeScript e Web Storage, incluindo exemplos práticos.

YouTube Video

TypeScript e Web Storage

O Web Storage do navegador é um armazenamento chave/valor para strings. É leve, com uma API síncrona, mas tenha em mente que pode armazenar apenas strings e que você deve tratar exceções, como exceder a cota de armazenamento. Quando combinado com TypeScript, você pode adicionar segurança de tipos, serialização/desserialização seguras, gerenciamento centralizado de chaves e expiração e versionamento, resultando em um design pronto para produção.

localStorage e sessionStorage

localStorage é um armazenamento persistente que permanece após fechar o navegador, enquanto sessionStorage é um armazenamento de sessão por aba/janela que é limpo quando a aba é fechada. Ambos armazenam valores como pares chave-valor (strings).

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 é um exemplo para salvar e recuperar strings. Como o Web Storage pode armazenar apenas strings, objetos devem ser convertidos para JSON para serem armazenados.

Exemplo usando análise de JSON

Para armazenar e restaurar objetos no Web Storage, use 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 é um exemplo mínimo funcional. Em aplicações reais, você também deve considerar exceções como falhas de parse e esgotamento da cota de armazenamento.

Exemplo de tratamento de exceções para parsing de JSON

Aqui, fornecemos um wrapper para tornar leituras e gravações seguras e tratar falhas de JSON.parse e exceções 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}
  • Você pode reutilizar essa utilidade em todo o aplicativo. Encapsule ainda mais quando quiser adicionar try/catch.

Exemplo de salvamento com TTL (expiração)

Como o próprio Web Storage não tem TTL, gerencie adicionando um expiresAt ao 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}
  • TTL é eficaz para cache e salvamento automático (rascunhos) e pode reduzir inconsistências.

Exemplo de gerenciamento centralizado de chaves e uso de namespace (evitar colisões)

Padronizar as chaves como prefixo + versão + nome reduz colisões e simplifica migrações.

 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 um namespace na chave facilita alternar versões ou realizar limpeza posteriormente.

Exemplo de cota excedida (QuotaExceededError) e estratégias de fallback

Considere que um QuotaExceededError pode ocorrer ao executar setItem e projete uma estratégia de fallback para quando a gravação de dados falhar. Por exemplo, ao exceder a capacidade de armazenamento, você pode excluir dados antigos ou recorrer ao sessionStorage ou a um cache em memória para manter a estabilidade geral do aplicativo.

 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");
  • Dependendo do destino de fallback, a persistência dos dados pode não ser garantida. Portanto, escolha um destino de armazenamento apropriado de acordo com o seu caso de uso. Por exemplo, no modo de navegação privada ou sob limites de armazenamento, você pode manter a funcionalidade usando temporariamente a memória ou o sessionStorage.

Exemplo de sincronização entre abas (evento storage) e notificações na mesma aba

Usando window.addEventListener('storage', …), você pode detectar alterações no armazenamento que ocorreram em outras abas. No entanto, esse evento não é disparado na mesma aba. Portanto, para notificações de mudanças dentro da mesma aba, publique seus próprios 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});
  • Com este código, você pode sincronizar alterações no armazenamento tanto entre outras abas quanto na aba atual.

Registro com segurança de tipos (tipagem estrita por chave)

Defina um mapa de chave para tipo em TypeScript para evitar erros durante o salvamento e a recuperação.

 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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}

Serialização/reviver para tipos complexos (Date/Map etc.)

Para restaurar corretamente objetos como Date e Map, aproveite o replacer e o reviver do 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 esse método, objetos Date são restaurados corretamente como Date. Da mesma forma, Map e Set também podem ser marcados e restaurados.

Exemplo de versionamento e estratégias de migração

Se você puder mudar o formato de armazenamento no futuro, inclua uma versão no payload e prepare migrações.

 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}
  • Empilhando pequenas migrações, você pode manter a compatibilidade com versões anteriores.

Exemplo de tratamento em SSR (server-side rendering)

Em ambientes sem window, referenciar diretamente localStorage causará falhas, então use guardas de 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");
  • Para código que deve suportar SSR, lembre-se sempre de verificar typeof window.

Dicas práticas

  • Limite a frequência de gravações com debounce/throttle (para mitigar rajadas vindas de ações da UI).
  • Gerencie as chaves com um namespace + versão, por exemplo, app:v1:....
  • Como regra, não armazene informações sensíveis (tokens de acesso, etc.). Se for inevitável, considere tempos de vida curtos mais validação no servidor ou WebCrypto.
  • A capacidade depende do navegador (alguns MB), então armazene dados grandes no IndexedDB.
  • Use CustomEvent para notificações na mesma aba e storage para notificações entre abas.
  • Em SSR, sempre verifique typeof window.

Classe 'type-safe store' consolidada

Vamos analisar um exemplo de implementação de uma classe genérica que integra os elementos que abordamos até agora, incluindo namespaces, segurança de tipos, expiração (TTL) e tratamento de exceções. Em produtos reais, considere adicionar testes, logging, exclusão de dados antigos baseada em LRU, criptografia e mais.

 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 mostra a configuração e as definições de tipos para construir um armazenamento chave-valor com segurança de tipos e rico em recursos.
 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 é o núcleo da classe TypedStorage, que fornece um armazenamento chave-valor com segurança de tipos. Ele gerencia as chaves permitidas e seus tipos com base no registry e gera chaves de armazenamento com prefixo. Além disso, ele usa localStorage e um fallback em memória, e permite definir um nome de evento para notificações de mudanças na mesma aba.
 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  }
  • O método get recupera o valor para a chave especificada de forma segura quanto ao tipo e pode, opcionalmente, lidar com valores com TTL (expiração).
 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  }
  • O método set salva um valor sob a chave especificada e retorna um booleano indicando se a operação foi bem-sucedida.
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  }
  • O método setWithTTL salva um valor com TTL (expiração).
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  }
  • O método remove exclui o valor da chave especificada tanto do armazenamento quanto do 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}
  • O método clear exclui todos os dados armazenados tanto no armazenamento quanto no 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
  • Este código é um exemplo usando TypedStorage, que salva e recupera valores como "theme" e "draft" em um armazenamento chave-valor com segurança de tipos e também oferece suporte a TTL e a um fallback. Ele configura notificações na mesma aba e um fallback em memória para realizar operações de armazenamento com segurança.

  • A classe TypedStorage é um ponto de partida prático. Implemente estratégias LRU, criptografia, compactação e fallbacks para IndexedDB conforme necessário.

Resumo

Ao usar Web Storage com TypeScript, tenha sempre em mente quatro pontos — segurança de tipos, resiliência a exceções, segurança e sincronização (múltiplas abas) — para um design robusto. Os wrappers e utilitários que vimos até agora são exemplos disso. Você também pode migrar para outros armazenamentos do navegador, como o IndexedDB, conforme necessário.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video