TypeScript og Web Storage
Denne artikkelen forklarer TypeScript og Web Storage.
Vi forklarer TypeScript og Web Storage, inkludert praktiske eksempler.
YouTube Video
TypeScript og Web Storage
Nettleserens Web Storage er en nøkkel/verdi-lagring for strenger. Det er lettvektig med et synkront API, men vær oppmerksom på at det kan lagre kun strenger, og at du må håndtere unntak som å overskride lagringskvoten. Når det kombineres med TypeScript, kan du legge til typesikkerhet, sikker serialisering/deserialisering, sentralisert nøkkelhåndtering samt utløp og versjonering, noe som resulterer i et produksjonsklart design.
localStorage og sessionStorage
localStorage er vedvarende lagring som blir værende etter at nettleseren lukkes, mens sessionStorage er øktlagring per fane/vindu som slettes når fanen lukkes. Begge lagrer verdier som nøkkel–verdi-par (strenger).
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"
- Denne koden er et eksempel på å lagre og hente strenger. Fordi
Web Storagekun kan lagre strenger, må objekter konverteres til JSON for å lagres.
Eksempel med JSON-parsing
For å lagre og gjenopprette objekter i Web Storage, bruk 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" }
- Denne koden er et minimalt fungerende eksempel. I virkelige applikasjoner må du også ta høyde for unntak som parse-feil og at lagringskvoten blir brukt opp.
Eksempel på unntakshåndtering for JSON-parsing
Her gir vi en wrapper for å gjøre lesing og skriving sikre, og håndtere JSON.parse-feil og setItem-unntak.
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 gjenbruke dette verktøyet i hele appen. Pakk ytterligere inn når du vil legge til
try/catch.
Eksempel på lagring med TTL (utløp)
Siden Web Storage i seg selv ikke har TTL, håndter det ved å legge til en expiresAt i verdien.
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 er effektivt for cache og autolagring (utkast) og kan redusere inkonsistenser.
Eksempel på sentralisert nøkkelhåndtering og navnerom (kollisjonsunngåelse)
Å standardisere nøkler som prefix + version + name reduserer kollisjoner og forenkler migreringer.
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};- Å inkludere et navneområde i nøkkelen gjør det enklere å bytte versjoner eller utføre opprydding senere.
Eksempel på kvoteoverskridelse (QuotaExceededError) og tilbakefallsstrategier
Ta høyde for at en QuotaExceededError kan oppstå når setItem kjøres, og utform en fallback-strategi for når lagring av data feiler. For eksempel, når du overskrider lagringskapasiteten, kan du slette gamle data eller falle tilbake til sessionStorage eller en minnebasert cache for å opprettholde appens generelle 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");- Avhengig av fallback-målet kan det hende at data ikke blir bevart. Velg derfor et passende lagringsmål i henhold til brukstilfellet ditt. For eksempel, i privat nettlesingsmodus eller under lagringsbegrensninger, kan du opprettholde funksjonalitet ved midlertidig å bruke minne eller
sessionStorage.
Eksempel på synkronisering mellom faner (storage-hendelse) og varsler i samme fane
Ved å bruke window.addEventListener('storage', …) kan du oppdage lagringsendringer som har skjedd i andre faner. Denne hendelsen utløses imidlertid ikke i samme fane. Derfor bør du for endringsvarsler i samme fane utløse dine egne hendelser ved å bruke 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});- Med denne koden kan du synkronisere lagringsendringer både på andre faner og i gjeldende fane.
Typesikkert register (streng typing per nøkkel)
Definer en nøkkel-til-type-mapping i TypeScript for å forhindre feil under lagring og henting.
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 for komplekse typer (Date/Map osv.)
For å gjenopprette objekter som Date og Map riktig, bruk replacer og reviver til 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 denne metoden blir
Date-objekter korrekt gjenopprettet somDate. Tilsvarende kanMapogSetogså merkes og gjenopprettes.
Eksempel på versjonering og migreringsstrategier
Hvis du kan komme til å endre lagringsformatet i fremtiden, inkluder en versjon i nyttelasten og forbered migreringer.
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}- Ved å stable små migreringer kan du opprettholde bakoverkompatibilitet.
Eksempel på håndtering i SSR (server-side rendering)
I miljøer uten window vil direkte referanser til localStorage krasje, så bruk miljøsjekker (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");- For kode som må støtte SSR, husk alltid å sjekke
typeof window.
Praktiske tips
- Begrens skrivefrekvens med debounce/throttle (for å dempe topper fra UI-handlinger).
- Administrer nøkler med navnerom + versjon, f.eks.
app:v1:.... - Ikke lagre sensitiv informasjon (tilgangstoken o.l.) som hovedregel. Hvis du må, vurder korte levetider pluss servervalidering eller WebCrypto.
- Kapasiteten avhenger av nettleseren (noen MB), så lagre store data i IndexedDB.
- Bruk
CustomEventfor varsler i samme fane ogstoragefor på tvers av faner. - I SSR, sjekk alltid
typeof window.
Konsolidert «typesikker store»-klasse
La oss se på en eksempelimplementasjon av en generisk klasse som integrerer elementene vi har dekket så langt, inkludert navnerom, typesikkerhet, utløp (TTL) og unntakshåndtering. I faktiske produkter bør du vurdere å legge til tester, logging, LRU-basert sletting av gamle data, kryptering og mer.
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}- Denne koden viser konfigurasjon og typedefinisjoner for å bygge et typesikkert, funksjonsrikt nøkkel-verdi-lager.
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; }- Denne koden er kjernen i klassen
TypedStoragesom tilbyr typesikker nøkkel-verdi-lagring. Den håndterer de tillatte nøklene og typene deres basert påregistry, og genererer lagringsnøkler med prefiks. Videre bruker denlocalStorageog en minnebasert fallback, og lar deg angi et hendelsesnavn for endringsvarsler i samme fane.
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
gethenter verdien for den angitte nøkkelen på en typesikker måte og kan valgfritt håndtere verdier med TTL (utløp).
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
setlagrer en verdi under den angitte nøkkelen og returnerer en boolsk verdi som angir om det lyktes.
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
setWithTTLlagrer en verdi med TTL (utløp).
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
removesletter verdien for den angitte nøkkelen fra både lagringen og fallback-løsningen.
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
clearsletter alle data som er lagret i både lagringen og fallback-løsningen.
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
-
Denne koden er et eksempel på bruk av
TypedStorage, som lagrer og henter verdier som"theme"og"draft"i et typesikkert nøkkel-verdi-lager, og støtter også TTL og en fallback-løsning. Den konfigurerer varsler i samme fane og en minnebasert fallback for å utføre lagringsoperasjoner på en trygg måte. -
Klassen
TypedStorageer et praktisk utgangspunkt. Implementer LRU-strategier, kryptering, komprimering og tilbakefall til IndexedDB etter behov.
Sammendrag
Når du bruker Web Storage med TypeScript, husk alltid fire punkter – typesikkerhet, robusthet mot unntak, sikkerhet og synkronisering (flere faner) – for et robust design. Wrapperne og verktøyene vi har sett så langt er eksempler på dette. Du kan også migrere til annen nettleserlagring som IndexedDB ved behov.
Du kan følge med på artikkelen ovenfor ved å bruke Visual Studio Code på vår YouTube-kanal. Vennligst sjekk ut YouTube-kanalen.