TypeScript und Web Storage

TypeScript und Web Storage

Dieser Artikel erklärt TypeScript und Web Storage.

Wir erklären TypeScript und Web Storage, einschließlich praktischer Beispiele.

YouTube Video

TypeScript und Web Storage

Der Web Storage des Browsers ist ein Schlüssel-Wert-Speicher für Strings. Es ist leichtgewichtig mit einer synchronen API, aber beachten Sie, dass es nur Zeichenketten speichern kann und dass Sie Ausnahmen wie das Überschreiten des Speicherkontingents behandeln müssen. In Kombination mit TypeScript können Sie Typsicherheit, sichere Serialisierung/Deserialisierung, zentrale Schlüsselverwaltung sowie Ablauf und Versionierung hinzufügen, was zu einem produktionsreifen Design führt.

localStorage und sessionStorage

localStorage ist ein persistenter Speicher, der auch nach dem Schließen des Browsers erhalten bleibt, während sessionStorage ein pro Tab/Fenster gültiger Sitzungs-Speicher ist, der beim Schließen des Tabs geleert wird. Beide speichern Werte als Schlüssel-Wert-Paare (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"
  • Dieser Code ist ein Beispiel zum Speichern und Abrufen von Zeichenketten. Da Web Storage nur Strings speichern kann, müssen Objekte zum Speichern in JSON konvertiert werden.

Beispiel mit JSON-Parsing

Um Objekte in Web Storage zu speichern und wiederherzustellen, verwenden Sie 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" }
  • Dieser Code ist ein minimal lauffähiges Beispiel. In realen Anwendungen müssen Sie auch Ausnahmen wie Parse-Fehler und das Ausschöpfen des Speicherkontingents berücksichtigen.

Beispiel für Ausnahmebehandlung beim JSON-Parsing

Hier stellen wir einen Wrapper bereit, der Lese- und Schreibvorgänge absichert und JSON.parse-Fehler sowie setItem-Ausnahmen behandelt.

 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}
  • Sie können diese Hilfsfunktion in der gesamten App wiederverwenden. Fügen Sie bei Bedarf weitere Wrapper mit try/catch hinzu.

Beispiel für Speichern mit TTL (Ablaufzeit)

Da Web Storage selbst keine TTL hat, verwalten Sie sie, indem Sie dem Wert ein expiresAt hinzufügen.

 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 ist effektiv für Cache und Autosave (Entwürfe) und kann Inkonsistenzen verringern.

Beispiel für zentrales Schlüssel-Management und Namespacing (Kollisionsvermeidung)

Die Standardisierung von Schlüsseln als Präfix + Version + Name reduziert Kollisionen und vereinfacht Migrationen.

 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};
  • Die Verwendung eines Namensraums im Schlüssel erleichtert spätere Versionswechsel oder Bereinigungen.

Beispiel für überschrittenes Kontingent (QuotaExceededError) und Fallback-Strategien

Beachten Sie, dass beim Ausführen von setItem ein QuotaExceededError auftreten kann, und entwickeln Sie eine Fallback-Strategie für den Fall, dass das Speichern fehlschlägt. Wenn beispielsweise die Speicherkapazität überschritten wird, können Sie alte Daten löschen oder auf sessionStorage bzw. einen In-Memory-Cache zurückfallen, um die Stabilität der Anwendung insgesamt zu gewährleisten.

 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");
  • Je nach Fallback-Ziel ist die Persistenz der Daten möglicherweise nicht gewährleistet. Wählen Sie daher je nach Anwendungsfall ein geeignetes Speicherziel. Zum Beispiel können Sie im privaten Browsing-Modus oder bei Speichereinschränkungen die Funktionalität aufrechterhalten, indem Sie vorübergehend den Arbeitsspeicher oder sessionStorage verwenden.

Beispiel für Tab-übergreifende Synchronisierung (storage-Event) und Benachrichtigungen im selben Tab

Mit window.addEventListener('storage', …) können Sie Speicheränderungen erkennen, die in anderen Tabs aufgetreten sind. Dieses Ereignis wird jedoch nicht im selben Tab ausgelöst. Daher sollten Sie für Änderungsbenachrichtigungen im selben Tab eigene Events mit CustomEvent veröffentlichen.

 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});
  • Mit diesem Code können Sie Speicheränderungen sowohl über andere Tabs als auch im aktuellen Tab synchronisieren.

Typsichere Registry (strikte Typisierung pro Schlüssel)

Definieren Sie in TypeScript eine Zuordnung von Schlüsseln zu Typen, um Fehler beim Speichern und Abrufen zu vermeiden.

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

Serialisierung/Reviver für komplexe Typen (Date/Map usw.)

Um Objekte wie Date und Map korrekt wiederherzustellen, nutzen Sie den replacer und reviver von 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}
  • Mit dieser Methode werden Date-Objekte korrekt als Date wiederhergestellt. Ebenso können Map und Set markiert und wiederhergestellt werden.

Beispiel für Versionierung und Migrationsstrategien

Wenn sich das Speicherformat künftig ändern könnte, fügen Sie der Nutzlast eine Version hinzu und bereiten Sie Migrationen vor.

 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}
  • Durch das Stapeln kleiner Migrationen können Sie die Abwärtskompatibilität wahren.

Beispiel für den Umgang in SSR (Server-Side Rendering)

In Umgebungen ohne window führt ein direkter Verweis auf localStorage zum Absturz – verwenden Sie daher Umgebungsprüfungen (Guards).

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");
  • Für Code, der SSR unterstützen muss, denken Sie immer daran, typeof window zu prüfen.

Praktische Tipps

  • Begrenzen Sie die Schreibfrequenz mit Debounce/Throttle (um Schübe durch UI-Aktionen abzufedern).
  • Verwalten Sie Schlüssel mit Namespace + Version, z. B. app:v1:....
  • Speichern Sie grundsätzlich keine sensiblen Informationen (Access Tokens usw.). Wenn es sein muss, erwägen Sie kurze Lebensdauern plus Servervalidierung oder WebCrypto.
  • Die Kapazität hängt vom Browser ab (einige MB), daher sollten große Daten in IndexedDB gespeichert werden.
  • Verwenden Sie CustomEvent für Benachrichtigungen im selben Tab und storage für Tab-übergreifende Ereignisse.
  • In SSR sollten Sie stets typeof window prüfen.

Konsolidierte 'typsichere Store'-Klasse

Schauen wir uns eine Beispielimplementierung einer generischen Klasse an, die die bisher behandelten Elemente integriert, darunter Namensräume, Typsicherheit, Ablaufzeit (TTL) und Ausnahmebehandlung. In realen Produkten sollten Sie Tests, Logging, LRU-basierte Löschung alter Daten, Verschlüsselung und Weiteres hinzufügen.

 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}
  • Dieser Code zeigt die Konfiguration und Typdefinitionen zum Aufbau eines typsicheren, funktionsreichen Key-Value-Speichers.
 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; }
  • Dieser Code ist der Kern der Klasse TypedStorage, die einen typsicheren Schlüssel-Wert-Speicher bereitstellt. Sie verwaltet die zulässigen Schlüssel und deren Typen basierend auf der registry und erzeugt präfixierte Speicherschlüssel. Weiterhin verwendet sie localStorage und einen In-Memory-Fallback und ermöglicht das Festlegen eines Ereignisnamens für Benachrichtigungen über Änderungen im selben 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  }
  • Die Methode get ruft den Wert für den angegebenen Schlüssel typsicher ab und kann optional Werte mit einer Ablaufzeit (TTL) behandeln.
 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  }
  • Die Methode set speichert einen Wert unter dem angegebenen Schlüssel und gibt einen booleschen Wert zurück, der angibt, ob der Vorgang erfolgreich war.
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  }
  • Die Methode setWithTTL speichert einen Wert mit einer Ablaufzeit (TTL).
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  }
  • Die Methode remove löscht den Wert für den angegebenen Schlüssel sowohl aus dem Speicher als auch aus dem 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}
  • Die Methode clear löscht alle Daten, die sowohl im Speicher als auch im Fallback gespeichert sind.
 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
  • Dieser Code ist ein Beispiel für die Verwendung von TypedStorage, der Werte wie "theme" und "draft" in einem typsicheren Schlüssel-Wert-Speicher speichert und abruft und außerdem TTL und einen Fallback unterstützt. Der Code konfiguriert Benachrichtigungen im selben Tab und einen In-Memory-Fallback, um Speicheroperationen sicher auszuführen.

  • Die Klasse TypedStorage ist ein praktischer Ausgangspunkt. Implementieren Sie bei Bedarf LRU-Strategien, Verschlüsselung, Komprimierung und Fallbacks auf IndexedDB.

Zusammenfassung

Beim Einsatz von Web Storage mit TypeScript sollten Sie für ein robustes Design stets vier Punkte im Blick behalten—Typsicherheit, Ausnahmerobustheit, Sicherheit und Synchronisierung (mehrere Tabs). Die bisher gezeigten Wrapper und Hilfsfunktionen sind Beispiele dafür. Sie können bei Bedarf auch auf andere Browserspeicher wie IndexedDB migrieren.

Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.

YouTube Video