TypeScript ve Web Storage

TypeScript ve Web Storage

Bu makale TypeScript ve Web Storage'ı açıklar.

Uygulamalı örneklerle birlikte TypeScript ve Web Storage'ı açıklıyoruz.

YouTube Video

TypeScript ve Web Storage

Tarayıcının Web Storage'ı, string'ler için anahtar/değer depolamasıdır. Hafiftir ve eşzamanlı bir API sunar; ancak yalnızca dizeler (string) depolayabildiğini ve depolama kotasının aşılması gibi istisnaları ele almanız gerektiğini aklınızda bulundurun. TypeScript ile birleştirildiğinde, tür güvenliği, güvenli serileştirme/deserileştirme, merkezi anahtar yönetimi ve sona erme ile sürümleme ekleyebilir, bunun sonucunda üretime hazır bir tasarım elde edebilirsiniz.

localStorage ve sessionStorage

localStorage, tarayıcı kapatıldıktan sonra da kalan kalıcı bir depolamadır; sessionStorage ise sekme/pencere başına çalışan ve sekme kapatıldığında temizlenen oturum depolamasıdır. Her ikisi de değerleri anahtar-değer (string) çiftleri olarak saklar.

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"
  • Bu kod, string'leri kaydetme ve geri alma için bir örnektir. Web Storage yalnızca string saklayabildiğinden, nesnelerin saklanmadan önce JSON'a dönüştürülmesi gerekir.

JSON ayrıştırması kullanan örnek

Web Storage içinde nesneleri depolamak ve geri yüklemek için JSON.stringify / JSON.parse kullanın.

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" }
  • Bu kod minimum çalışan bir örnektir. Gerçek uygulamalarda, ayrıştırma hataları ve depolama kotasının tükenmesi gibi istisnaları da hesaba katmalısınız.

JSON ayrıştırması için istisna işleme örneği

Burada, okumaları ve yazmaları güvenli hale getiren ve JSON.parse hataları ile setItem istisnalarını ele alan bir sarmalayıcı sunuyoruz.

 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}
  • Bu yardımcıyı uygulamanın genelinde yeniden kullanabilirsiniz. try/catch eklemek istediğinizde ek bir sarmalayıcıyla sarmalayın.

TTL (sona erme) ile kaydetme örneği

Web Storage'ın kendisinde TTL olmadığından, değere bir expiresAt ekleyerek yönetin.

 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, önbellek ve otomatik kaydetme (taslaklar) için etkilidir ve tutarsızlıkları azaltabilir.

Merkezi anahtar yönetimi ve ad alanı (çakışmayı önleme) örneği

Anahtarları prefix + version + name olarak standartlaştırmak çakışmaları azaltır ve geçişleri basitleştirir.

 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};
  • Anahtara bir ad alanı eklemek, sürümler arasında geçiş yapmayı veya daha sonra temizlik yapmayı kolaylaştırır.

Kota aşıldı (QuotaExceededError) durumunun ve geri dönüş stratejilerinin örneği

setItem çalıştırılırken bir QuotaExceededError oluşabileceğini göz önünde bulundurun ve veri kaydetme başarısız olduğunda devreye girecek bir geri dönüş (fallback) stratejisi tasarlayın. Örneğin depolama kapasitesini aştığınızda, genel uygulama kararlılığını korumak için eski verileri silebilir veya sessionStorage ya da bellek içi bir önbelleğe geri dönebilirsiniz.

 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");
  • Geri dönüş (fallback) hedefine bağlı olarak veri kalıcılığı garanti edilmeyebilir. Bu nedenle, kullanım durumunuza göre uygun bir depolama hedefi seçin. Örneğin gizli gezinme modunda veya depolama kısıtları altında, geçici olarak belleği ya da sessionStorage'ı kullanarak işlevselliği sürdürebilirsiniz.

Sekmeler arası senkronizasyon (storage olayı) ve aynı sekme bildirimleri örneği

window.addEventListener('storage', …) kullanarak diğer sekmelerde gerçekleşen depolama değişikliklerini algılayabilirsiniz. Ancak bu olay aynı sekme içinde tetiklenmez. Bu nedenle, aynı sekme içindeki değişiklik bildirimleri için CustomEvent kullanarak kendi olaylarınızı yayımlayın.

 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});
  • Bu kodla, depolama değişikliklerini hem diğer sekmeler hem de mevcut sekme arasında senkronize edebilirsiniz.

Tür güvenli kayıt (anahtar başına katı türleme)

Kaydetme ve geri alma sırasında hataları önlemek için TypeScript'te anahtar-tür eşlemesi tanımlayı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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}

Karmaşık tipler için serileştirme/yeniden canlandırıcı (Date/Map vb.)

Date ve Map gibi nesneleri doğru şekilde geri yüklemek için JSON.stringify'nin replacer ve reviver özelliklerinden yararlanın.

 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}
  • Bu yöntemle, Date nesneleri doğru şekilde Date olarak geri yüklenir. Benzer şekilde, Map ve Set de işaretlenip geri yüklenebilir.

Sürümleme ve geçiş stratejileri örneği

Gelecekte depolama biçimini değiştirebilecekseniz, verinin içine bir sürüm ekleyin ve geçişleri hazırlayın.

 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}
  • Küçük geçişleri ardışık uygulayarak geriye dönük uyumluluğu koruyabilirsiniz.

SSR'de (server-side rendering) ele alma örneği

window olmayan ortamlarda localStorage'a doğrudan başvurmak çöküşe yol açar; bu yüzden ortam korumaları kullanın.

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");
  • SSR'yi desteklemesi gereken kod için, her zaman typeof window kontrolünü yapmayı unutmayın.

Pratik ipuçları

  • Yazma sıklığını debounce/throttle ile sınırlayın (UI eylemlerinden kaynaklanan ani artışları azaltmak için).
  • Anahtarları ad alanı + sürüm ile yönetin; örn. app:v1:....
  • Kural olarak hassas bilgileri (erişim belirteçleri vb.) depolamayın. Zorunluysa, kısa yaşam sürelerini ve sunucu doğrulamasını veya WebCrypto'yu düşünün.
  • Kapasite tarayıcıya bağlıdır (birkaç MB); bu nedenle büyük verileri IndexedDB'de saklayın.
  • Aynı sekme bildirimleri için CustomEvent, sekmeler arası için storage kullanın.
  • SSR'de her zaman typeof window kontrol edin.

Birleştirilmiş 'tür güvenli depolama' sınıfı

Ad alanları, tür güvenliği, geçerlilik süresi (TTL) ve istisna işleme gibi şimdiye kadar ele aldığımız unsurları birleştiren jenerik bir sınıfın örnek bir uygulamasına bakalım. Gerçek ürünlerde testler, günlükleme, LRU tabanlı eski veri silme, şifreleme ve daha fazlasını eklemeyi düşünün.

 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}
  • Bu kod, tür güvenli, zengin özellikli bir anahtar-değer deposu oluşturmak için yapılandırmayı ve tür tanımlarını gösterir.
 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; }
  • Bu kod, tür güvenli anahtar-değer depolaması sağlayan TypedStorage sınıfının çekirdeğidir. registry'ye göre izin verilen anahtarları ve bunların türlerini yönetir ve önekli depolama anahtarları üretir. Ayrıca localStorage ve bellek içi bir geri dönüş (fallback) kullanır ve aynı sekme değişiklik bildirimleri için bir olay adı belirlemenize olanak tanır.
 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  }
  • get metodu, belirtilen anahtar için değeri tür güvenli bir şekilde alır ve isteğe bağlı olarak TTL (geçerlilik süresi) olan değerleri de işleyebilir.
 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  }
  • set metodu, belirtilen anahtar altında bir değeri kaydeder ve başarılı olup olmadığını belirten bir boolean döndürür.
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  }
  • setWithTTL metodu, bir değeri TTL (geçerlilik süresi) ile kaydeder.
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  }
  • remove metodu, belirtilen anahtarın değerini hem depolamadan hem de yedek depolamadan siler.
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}
  • clear metodu, hem depolamada hem de yedek depolamada tutulan tüm verileri siler.
 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
  • Bu kod, TypedStorage kullanımına bir örnektir; "theme" ve "draft" gibi değerleri tür güvenli bir anahtar-değer deposunda kaydedip alır ve ayrıca TTL ile yedek depolamayı da destekler. Depolama işlemlerini güvenli bir şekilde gerçekleştirmek için aynı sekme bildirimlerini ve bellek içi bir geri dönüşü (fallback) yapılandırır.

  • TypedStorage sınıfı pratik bir başlangıç noktasıdır. Gerektiğinde LRU stratejilerini, şifrelemeyi, sıkıştırmayı ve IndexedDB'ye geri dönüşleri uygulayın.

Özet

TypeScript ile Web Storage kullanırken sağlam bir tasarım için dört noktayı her zaman aklınızda bulundurun: tür güvenliği, istisna dayanıklılığı, güvenlik ve senkronizasyon (birden çok sekme). Şimdiye kadar gördüğümüz sarmalayıcılar ve yardımcılar buna örnektir. Gerektiğinde IndexedDB gibi diğer tarayıcı depolamalarına da geçiş yapabilirsiniz.

Yukarıdaki makaleyi, YouTube kanalımızda Visual Studio Code'u kullanarak takip edebilirsiniz. Lütfen YouTube kanalını da kontrol edin.

YouTube Video