TypeScript at Web Storage

TypeScript at Web Storage

Ipinaliliwanag ng artikulong ito ang TypeScript at Web Storage.

Ipinaliliwanag namin ang TypeScript at Web Storage, kasama ang mga praktikal na halimbawa.

YouTube Video

TypeScript at Web Storage

Ang Web Storage ng browser ay imbakan ng key/value para sa mga string. Magaan ito na may synchronous API, ngunit tandaan na mga string lamang ang kaya nitong iimbak, at kailangan mong hawakan ang mga exception gaya ng paglampas sa storage quota. Kapag pinagsama sa TypeScript, maaari kang magdagdag ng type safety, ligtas na serialization/deserialization, sentralisadong pamamahala ng key, at expiration at versioning, na nagreresulta sa disenyong handa para sa production.

localStorage at sessionStorage

localStorage ay persistent storage na nananatili kahit isara ang browser, samantalang ang sessionStorage ay per-tab/window na session storage na nabubura kapag isinara ang tab. Pareho silang nag-iimbak ng mga value bilang key-value pairs (mga string).

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"
  • Ang code na ito ay isang halimbawa para sa pag-iimbak at pagkuha ng mga string. Dahil mga string lamang ang maiimbak sa Web Storage, kailangang i-convert ang mga object sa JSON upang maiimbak.

Halimbawa gamit ang JSON parsing

Upang mag-imbak at magbalik ng mga object sa Web Storage, gamitin ang 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" }
  • Ang code na ito ay isang minimal na gumaganang halimbawa. Sa mga totoong aplikasyon, kailangan mo ring isaalang-alang ang mga exception tulad ng mga pagkabigo sa pag-parse at pagkaubos ng storage quota.

Halimbawa ng paghawak ng exception para sa JSON parsing

Dito, nagbibigay kami ng isang wrapper upang gawing ligtas ang pagbasa at pagsulat at para hawakan ang mga pagkabigo ng JSON.parse at mga exception ng 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}
  • Maaari mong i-reuse ang utility na ito sa buong app. I-wrap pa kapag nais mong magdagdag ng try/catch.

Halimbawa ng pag-save na may TTL (expiration)

Dahil walang TTL ang Web Storage mismo, pamahalaan ito sa pamamagitan ng pagdaragdag ng expiresAt sa value.

 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}
  • Epektibo ang TTL para sa cache at autosave (mga draft) at makababawas sa mga hindi pagkakatugma.

Halimbawa ng sentralisadong pamamahala ng key at namespacing (pag-iwas sa banggaan)

Ang pag-standardize ng mga key bilang prefix + version + name ay nagpapababa ng mga banggaan at nagpapasimple ng mga migration.

 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};
  • Ang paglalagay ng namespace sa key ay nagpapadali sa paglipat ng mga bersyon o sa paglilinis paglaon.

Halimbawa ng quota exceeded (QuotaExceededError) at mga fallback strategy

Isaalang-alang na maaaring mangyari ang QuotaExceededError kapag tinatawag ang setItem, at magdisenyo ng isang estratehiyang fallback para sa mga pagkakataong mabigo ang pag-save ng data. Halimbawa, kapag lumampas ka sa kapasidad ng storage, maaari mong burahin ang lumang data o lumipat sa sessionStorage o isang in-memory na cache upang mapanatili ang pangkalahatang katatagan ng 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");
  • Depende sa patutunguhan ng fallback, maaaring hindi matiyak ang pagpapanatili ng data. Kaya, pumili ng naaangkop na destinasyon ng storage ayon sa iyong use case. Halimbawa, sa private browsing mode o kapag may mga limitasyon sa storage, maaari mong mapanatili ang functionality sa pamamagitan ng pansamantalang paggamit ng memorya o sessionStorage.

Halimbawa ng cross-tab sync (kaganapang storage) at mga abiso sa parehong tab

Gamit ang window.addEventListener('storage', …), maaari mong matukoy ang mga pagbabagong naganap sa storage sa ibang mga tab. Gayunpaman, hindi nagti-trigger ang event na ito sa parehong tab. Kaya, para sa mga abiso ng pagbabago sa loob ng parehong tab, i-publish ang sarili mong mga event gamit ang 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});
  • Gamit ang code na ito, maaari mong i-synchronize ang mga pagbabago sa storage sa parehong ibang mga tab at ang kasalukuyang tab.

Type-safe registry (mahigpit na typing kada key)

Magtakda ng key-to-type map sa TypeScript upang maiwasan ang mga pagkakamali sa pag-save at pagkuha.

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

Serialization/reviver para sa mga komplikadong type (Date/Map, atbp.)

Upang maibalik nang tama ang mga object tulad ng Date at Map, gamitin ang replacer at reviver ng 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}
  • Gamit ang paraang ito, ang mga Date object ay naibabalik nang tama bilang Date. Sa katulad na paraan, maaaring markahan at maibalik din ang Map at Set.

Halimbawa ng versioning at mga strategy ng migration

Kung maaaring magbago ang format ng storage sa hinaharap, isama ang isang version sa payload at maghanda ng mga migration.

 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}
  • Sa pamamagitan ng pagsasalansan ng maliliit na migration, mapapanatili mo ang backward compatibility.

Halimbawa ng paghawak sa SSR (server-side rendering)

Sa mga kapaligirang walang window, ang direktang pagtukoy sa localStorage ay magka-crash, kaya gumamit ng mga environment guard.

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 sa code na dapat sumuporta sa SSR, laging tandaan na i-check ang typeof window.

Praktikal na mga tip

  • Limitahan ang dalas ng pagsulat gamit ang debounce/throttle (upang maibsan ang biglaang bugso mula sa mga UI action).
  • Pamahalaan ang mga key gamit ang namespace + version, hal., app:v1:....
  • Huwag mag-imbak ng sensitibong impormasyon (access tokens, atbp.) bilang patakaran. Kung kinakailangan, isaalang-alang ang maiikling lifetime kasama ng server validation o WebCrypto.
  • Ang kapasidad ay nakadepende sa browser (ilang MB), kaya ilagay ang malalaking data sa IndexedDB.
  • Gumamit ng CustomEvent para sa mga abiso sa parehong tab at storage para sa cross-tab.
  • Sa SSR, laging i-check ang typeof window.

Pinagsamang 'type-safe store' na klase

Tingnan natin ang isang halimbawa ng implementasyon ng isang generic na klase na pinagsasama ang mga elementong natalakay na natin hanggang ngayon, kabilang ang mga namespace, type safety, pag-expire (TTL), at paghawak ng exception. Sa mga aktwal na produkto, isaalang-alang ang pagdaragdag ng mga test, logging, LRU-based na pagbura ng lumang data, encryption, at iba pa.

 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}
  • Ipinapakita ng code na ito ang configuration at mga type definition para sa pagbuo ng isang type-safe, feature-rich na key-value storage.
 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; }
  • Ang code na ito ang ubod ng klaseng TypedStorage na nagbibigay ng ligtas-sa-uri na imbakan ng key-value. Pinamamahalaan nito ang mga pinahihintulutang susi at ang kanilang mga uri batay sa registry, at bumubuo ng mga susi sa imbakan na may prefix. Dagdag pa, gumagamit ito ng localStorage at isang in-memory na fallback, at hinahayaan kang magtakda ng pangalan ng event para sa mga abiso ng pagbabago sa parehong tab.
 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  }
  • Kinukuha ng method na get ang halaga para sa tinukoy na susi sa paraang ligtas sa uri at maaari ring opsyonal na humawak ng mga halagang may TTL (pag-expire).
 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  }
  • Iniimbak ng method na set ang isang halaga sa ilalim ng tinukoy na susi at nagbabalik ng isang boolean na nagsasaad kung nagtagumpay ito.
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  }
  • Iniimbak ng method na setWithTTL ang isang halaga na may TTL (pag-expire).
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  }
  • Binubura ng method na remove ang halaga para sa tinukoy na susi mula sa parehong imbakan at 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}
  • Binubura ng method na clear ang lahat ng datos na nakaimbak sa parehong imbakan at 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
  • Ang code na ito ay isang halimbawa ng paggamit ng TypedStorage, na nag-iimbak at kumukuha ng mga halagang gaya ng "theme" at "draft" sa isang ligtas-sa-uri na imbakan ng key-value, at sinusuportahan din ang TTL at isang fallback. Ito ay nagko-configure ng mga abiso sa parehong tab at isang in-memory na fallback upang maisagawa nang ligtas ang mga operasyon sa storage.

  • Ang klaseng TypedStorage ay isang praktikal na panimulang punto. Ipatupad ang mga LRU strategy, encryption, compression, at mga fallback sa IndexedDB ayon sa pangangailangan.

Buod

Kapag gumagamit ng Web Storage kasama ang TypeScript, laging tandaan ang apat na punto—type safety, katatagan laban sa exception, seguridad, at synchronization (maramihang tab)—para sa matibay na disenyo. Ang mga wrapper at utility na nakita natin sa ngayon ay mga halimbawa niyon. Maaari ka ring lumipat sa iba pang imbakan sa browser tulad ng IndexedDB kung kinakailangan.

Maaari mong sundan ang artikulo sa itaas gamit ang Visual Studio Code sa aming YouTube channel. Paki-check din ang aming YouTube channel.

YouTube Video