TypeScript och Web Storage
Den här artikeln förklarar TypeScript och Web Storage.
Vi förklarar TypeScript och Web Storage, inklusive praktiska exempel.
YouTube Video
TypeScript och Web Storage
Webbläsarens Web Storage är en nyckel/värde-lagring för strängar. Det är lättviktigt med ett synkront API, men tänk på att det kan lagra endast strängar, och att du måste hantera undantag såsom att lagringskvoten överskrids. I kombination med TypeScript kan du lägga till typsäkerhet, säker serialisering/deserialisering, centraliserad nyckelhantering samt förfallotid och versionering, vilket ger en produktionsfärdig lösning.
localStorage och sessionStorage
localStorage är persistent lagring som finns kvar efter att webbläsaren stängts, medan sessionStorage är per-flik/fönster-sessionslagring som rensas när fliken stängs. Båda lagrar värden som nyckel-värde-par (strängar).
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"
- Den här koden är ett exempel på att spara och hämta strängar. Eftersom
Web Storageendast kan lagra strängar måste objekt konverteras till JSON för att kunna lagras.
Exempel med JSON-parsning
För att lagra och återställa objekt i Web Storage, använd 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" }
- Den här koden är ett minimalt fungerande exempel. I verkliga applikationer måste du också ta hänsyn till undantag som parsningfel och att lagringskvoten tar slut.
Exempel på undantagshantering för JSON-parsning
Här tillhandahåller vi en wrapper som gör läsningar och skrivningar säkra och hanterar JSON.parse-fel och setItem-undantag.
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}- Du kan återanvända det här verktyget i hela appen. Wrappa ytterligare när du vill lägga till
try/catch.
Exempel på sparande med TTL (utgångstid)
Eftersom Web Storage i sig saknar TTL, hantera det genom att lägga till ett expiresAt i värdet.
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 är effektivt för cache och autosparning (utkast) och kan minska inkonsistenser.
Exempel på centraliserad nyckelhantering och namnrymder (undvika kollisioner)
Att standardisera nycklar som prefix + version + name minskar kollisioner och förenklar migreringar.
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};- Att inkludera en namnrymd i nyckeln gör det enklare att växla versioner eller städa upp senare.
Exempel på överskriden kvot (QuotaExceededError) och fallback-strategier
Tänk på att ett QuotaExceededError kan uppstå när setItem körs, och utforma en fallback-strategi för när det misslyckas att spara data. Till exempel, när du överskrider lagringskapaciteten kan du radera gammal data eller falla tillbaka till sessionStorage eller en cache i minnet för att bibehålla appens övergripande stabilitet.
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");- Beroende på vart du faller tillbaka kan datapersistens inte garanteras. Välj därför en lämplig lagringsdestination utifrån ditt användningsfall. Till exempel, i privat läge eller under lagringsbegränsningar kan du upprätthålla funktionalitet genom att tillfälligt använda minnet eller
sessionStorage.
Exempel på synk mellan flikar (händelsen storage) och aviseringar i samma flik
Med window.addEventListener('storage', …) kan du upptäcka lagringsändringar som inträffat i andra flikar. Denna händelse utlöses dock inte i samma flik. Publicera därför egna händelser med CustomEvent för ändringsnotiser inom samma flik.
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});- Med den här koden kan du synkronisera lagringsändringar både i andra flikar och i den aktuella fliken.
Typsäkert register (strikt typning per nyckel)
Definiera en nyckel-till-typ-mappning i TypeScript för att förhindra misstag vid sparande och hämtning.
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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}
Serialisering/reviver för komplexa typer (Date/Map m.fl.)
För att korrekt återställa objekt som Date och Map, utnyttja replacer och reviver i 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}- Med den här metoden återställs
Date-objekt korrekt somDate. På samma sätt kanMapochSetockså markeras och återställas.
Exempel på versionshantering och migrationsstrategier
Om du kan komma att ändra lagringsformatet i framtiden, inkludera en version i payloaden och förbered migreringar.
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}- Genom att stapla små migreringar kan du behålla bakåtkompatibilitet.
Exempel på hantering i SSR (server-side rendering)
I miljöer utan window kraschar direkta referenser till localStorage, så använd miljökontroller.
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 kod som måste stödja SSR, kom alltid ihåg att kontrollera
typeof window.
Praktiska tips
- Begränsa skrivfrekvens med debounce/throttle (för att dämpa spikar från UI-händelser).
- Hantera nycklar med namnrymd + version, t.ex.
app:v1:.... - Lagra som regel inte känslig information (åtkomsttoken m.m.). Om du måste, överväg kort livslängd plus servervalidering eller WebCrypto.
- Kapaciteten beror på webbläsaren (några MB), så lagra stora data i IndexedDB.
- Använd
CustomEventför aviseringar i samma flik ochstorageför mellan flikar. - I SSR, kontrollera alltid
typeof window.
Samlad 'typsäker store'-klass
Låt oss titta på en exempelimplementering av en generisk klass som integrerar de delar vi hittills har gått igenom, inklusive namnrymder, typsäkerhet, förfallotid (TTL) och undantagshantering. I verkliga produkter bör du överväga att lägga till tester, loggning, LRU-baserad radering av gammal data, kryptering med mera.
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}- Den här koden visar konfigurationen och typedefinitionerna för att bygga en typsäker, funktionsrik nyckel-värdeslagring.
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; }- Den här koden är kärnan i klassen
TypedStoragesom tillhandahåller typsäker nyckel-värde-lagring. Den hanterar tillåtna nycklar och deras typer baserat påregistryoch genererar lagringsnycklar med prefix. Dessutom använder denlocalStorageoch en fallback i minnet, och låter dig ange ett händelsenamn för ändringsnotiser i samma flik.
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 }- Metoden
gethämtar värdet för den angivna nyckeln på ett typsäkert sätt och kan vid behov hantera värden med TTL (förfallotid).
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 }- Metoden
setsparar ett värde under den angivna nyckeln och returnerar ett booleskt värde som anger om det lyckades.
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 }- Metoden
setWithTTLsparar ett värde med TTL (förfallotid).
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 }- Metoden
removetar bort värdet för den angivna nyckeln både från lagringen och från reservlagringen.
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}- Metoden
cleartar bort all data som lagrats både i lagringen och i reservlagringen.
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
-
Den här koden är ett exempel på hur man använder
TypedStorage, som sparar och hämtar värden såsom"theme"och"draft"i en typsäker nyckel-värde-lagring och dessutom stöder TTL och en reserv. Den konfigurerar notiser i samma flik och en fallback i minnet för att utföra lagringsoperationer på ett säkert sätt. -
Klassen
TypedStorageär en praktisk utgångspunkt. Implementera LRU-strategier, kryptering, komprimering och fallback till IndexedDB vid behov.
Sammanfattning
När du använder Web Storage med TypeScript, tänk alltid på fyra punkter – typsäkerhet, robusthet mot undantag, säkerhet och synkronisering (flera flikar) – för en robust design. De wrappers och verktyg vi har sett hittills är exempel på det. Du kan också migrera till annan webblagring såsom IndexedDB vid behov.
Du kan följa med i artikeln ovan med hjälp av Visual Studio Code på vår YouTube-kanal. Vänligen kolla även in YouTube-kanalen.