TypeScript dan Web Storage

TypeScript dan Web Storage

Artikel ini menjelaskan TypeScript dan Web Storage.

Kami menjelaskan TypeScript dan Web Storage, termasuk contoh-contoh praktis.

YouTube Video

TypeScript dan Web Storage

Web Storage pada peramban adalah penyimpanan kunci/nilai untuk string. Ringan dengan API sinkron, tetapi perlu diingat bahwa ini hanya dapat menyimpan string saja, dan Anda harus menangani pengecualian seperti saat kuota penyimpanan terlampaui. Jika digabungkan dengan TypeScript, Anda dapat menambahkan keamanan tipe, serialisasi/deserialisasi yang aman, manajemen kunci terpusat, serta kedaluwarsa dan versioning, sehingga menghasilkan desain yang siap produksi.

localStorage dan sessionStorage

localStorage adalah penyimpanan persisten yang tetap ada setelah peramban ditutup, sedangkan sessionStorage adalah penyimpanan sesi per tab/jendela yang akan dihapus saat tab ditutup. Keduanya menyimpan nilai sebagai pasangan kunci-nilai (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"
  • Kode ini adalah contoh untuk menyimpan dan mengambil string. Karena Web Storage hanya dapat menyimpan string, objek harus dikonversi ke JSON untuk disimpan.

Contoh menggunakan parsing JSON

Untuk menyimpan dan memulihkan objek dalam Web Storage, gunakan 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" }
  • Kode ini adalah contoh kerja minimal. Dalam aplikasi nyata, Anda juga harus memperhitungkan pengecualian seperti kegagalan parse dan kehabisan kuota penyimpanan.

Contoh penanganan pengecualian untuk parsing JSON

Di sini, kami menyediakan pembungkus (wrapper) untuk membuat operasi baca/tulis aman serta menangani kegagalan JSON.parse dan pengecualian 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}
  • Anda dapat menggunakan kembali utilitas ini di seluruh aplikasi. Bungkus lebih lanjut ketika Anda ingin menambahkan try/catch.

Contoh penyimpanan dengan TTL (kedaluwarsa)

Karena Web Storage sendiri tidak memiliki TTL, kelola dengan menambahkan expiresAt pada nilainya.

 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 efektif untuk cache dan penyimpanan otomatis (draft) serta dapat mengurangi inkonsistensi.

Contoh manajemen kunci terpusat dan penamaan ruang (namespacing) untuk menghindari tabrakan

Menstandarkan kunci sebagai prefix + version + name mengurangi tabrakan dan mempermudah migrasi.

 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};
  • Menyertakan namespace dalam kunci memudahkan penggantian versi atau pembersihan di kemudian hari.

Contoh kuota terlampaui (QuotaExceededError) dan strategi fallback

Pertimbangkan bahwa QuotaExceededError dapat terjadi saat menjalankan setItem, dan rancang strategi fallback untuk saat penyimpanan data gagal. Misalnya, ketika Anda melebihi kapasitas penyimpanan, Anda dapat menghapus data lama atau beralih ke sessionStorage atau cache dalam memori untuk menjaga stabilitas aplikasi secara keseluruhan.

 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");
  • Tergantung tujuan fallback, persistensi data mungkin tidak terjamin. Karena itu, pilih tujuan penyimpanan yang sesuai dengan kasus penggunaan Anda. Misalnya, dalam mode penjelajahan privat atau di bawah batas penyimpanan, Anda dapat mempertahankan fungsionalitas dengan sementara menggunakan memori atau sessionStorage.

Contoh sinkronisasi lintas tab (event storage) dan notifikasi dalam tab yang sama

Dengan menggunakan window.addEventListener('storage', …), Anda dapat mendeteksi perubahan penyimpanan yang terjadi di tab lain. Namun, event ini tidak dipicu di tab yang sama. Oleh karena itu, untuk pemberitahuan perubahan dalam tab yang sama, publikasikan event Anda sendiri menggunakan 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});
  • Dengan kode ini, Anda dapat menyinkronkan perubahan penyimpanan di tab lain maupun tab saat ini.

Registri yang aman terhadap tipe (pengetikan ketat per kunci)

Definisikan peta kunci-ke-tipe di TypeScript untuk mencegah kesalahan saat penyimpanan dan pengambilan.

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

Serialisasi/reviver untuk tipe kompleks (Date/Map, dll.)

Untuk memulihkan objek seperti Date dan Map dengan benar, manfaatkan replacer dan reviver dari 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}
  • Dengan metode ini, objek Date dipulihkan dengan benar sebagai Date. Demikian pula, Map dan Set juga dapat ditandai dan dipulihkan.

Contoh versioning dan strategi migrasi

Jika Anda mungkin akan mengubah format penyimpanan di masa depan, sertakan versi dalam payload dan siapkan migrasi.

 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}
  • Dengan menumpuk migrasi kecil, Anda dapat mempertahankan kompatibilitas mundur.

Contoh penanganan pada SSR (server-side rendering)

Di lingkungan tanpa window, merujuk localStorage secara langsung akan menyebabkan crash, jadi gunakan guard lingkungan.

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");
  • Untuk kode yang harus mendukung SSR, selalu ingat untuk memeriksa typeof window.

Tips praktis

  • Batasi frekuensi penulisan dengan debounce/throttle (untuk meredam lonjakan dari aksi UI).
  • Kelola kunci dengan namespace + versi, mis., app:v1:....
  • Sebagai aturan, jangan menyimpan informasi sensitif (access token, dll.). Jika terpaksa, pertimbangkan masa berlaku singkat ditambah validasi server atau WebCrypto.
  • Kapasitas bergantung pada peramban (beberapa MB), jadi simpan data besar di IndexedDB.
  • Gunakan CustomEvent untuk notifikasi dalam tab yang sama dan storage untuk lintas tab.
  • Dalam SSR, selalu periksa typeof window.

Kelas 'type-safe store' terpadu

Mari kita lihat contoh implementasi kelas generik yang mengintegrasikan elemen-elemen yang telah kita bahas sejauh ini, termasuk namespace, keamanan tipe, kedaluwarsa (TTL), dan penanganan pengecualian. Dalam produk nyata, pertimbangkan untuk menambahkan pengujian, pencatatan (logging), penghapusan data lama berbasis LRU, enkripsi, dan lainnya.

 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}
  • Kode ini menunjukkan konfigurasi dan definisi tipe untuk membangun penyimpanan key-value yang aman terhadap tipe dan kaya fitur.
 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; }
  • Kode ini adalah inti dari kelas TypedStorage yang menyediakan penyimpanan key-value dengan keamanan tipe. Kode ini mengelola kunci yang diizinkan dan tipe-tipe mereka berdasarkan registry, serta menghasilkan kunci penyimpanan berawalan (prefiks). Selain itu, ini menggunakan localStorage dan fallback dalam memori, serta memungkinkan Anda menetapkan nama event untuk notifikasi perubahan dalam tab yang sama.
 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  }
  • Metode get mengambil nilai untuk kunci yang ditentukan secara aman terhadap tipe dan secara opsional dapat menangani nilai dengan TTL (kedaluwarsa).
 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  }
  • Metode set menyimpan sebuah nilai pada kunci yang ditentukan dan mengembalikan nilai boolean yang menunjukkan apakah operasi berhasil.
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  }
  • Metode setWithTTL menyimpan sebuah nilai dengan TTL (kedaluwarsa).
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  }
  • Metode remove menghapus nilai untuk kunci yang ditentukan, baik dari penyimpanan maupun dari 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}
  • Metode clear menghapus semua data yang tersimpan, baik di penyimpanan maupun di 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
  • Kode ini adalah contoh penggunaan TypedStorage, yang menyimpan dan mengambil nilai seperti "theme" dan "draft" dalam penyimpanan key-value dengan keamanan tipe, serta mendukung TTL dan fallback. Ini mengonfigurasi notifikasi dalam tab yang sama dan fallback dalam memori untuk melakukan operasi penyimpanan dengan aman.

  • Kelas TypedStorage adalah titik awal yang praktis. Implementasikan strategi LRU, enkripsi, kompresi, dan fallback ke IndexedDB sesuai kebutuhan.

Ringkasan

Saat menggunakan Web Storage dengan TypeScript, selalu ingat empat poin—jaminan tipe, ketahanan terhadap pengecualian, keamanan, dan sinkronisasi (banyak tab)—untuk desain yang tangguh. Pembungkus dan utilitas yang telah kita lihat sejauh ini adalah contohnya. Anda juga dapat bermigrasi ke penyimpanan peramban lain seperti IndexedDB sesuai kebutuhan.

Anda dapat mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Silakan periksa juga saluran YouTube kami.

YouTube Video