TypeScript en Web Storage
Dit artikel legt TypeScript en Web Storage uit.
We leggen TypeScript en Web Storage uit, inclusief praktische voorbeelden.
YouTube Video
TypeScript en Web Storage
De Web Storage van de browser is een sleutel/waarde-opslag voor strings. Het is lichtgewicht met een synchrone API, maar houd er rekening mee dat het alleen strings kan opslaan en dat je exceptions moet afhandelen, zoals wanneer het opslagquotum wordt overschreden. In combinatie met TypeScript kun je typeveiligheid, veilige serialisatie/deserialisatie, gecentraliseerd sleutelbeheer en verval en versiebeheer toevoegen, wat resulteert in een productierijp ontwerp.
localStorage en sessionStorage
localStorage is persistente opslag die blijft na het sluiten van de browser, terwijl sessionStorage sessie-opslag per tab/venster is die wordt leeggemaakt wanneer de tab wordt gesloten. Beide slaan waarden op als sleutel-waardekoppels (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"
- Deze code is een voorbeeld voor het opslaan en ophalen van strings. Omdat
Web Storagealleen strings kan opslaan, moeten objecten naar JSON worden geconverteerd om ze op te slaan.
Voorbeeld met JSON-parsing
Om objecten op te slaan en te herstellen in Web Storage, gebruik je 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" }
- Deze code is een minimaal werkend voorbeeld. In echte toepassingen moet je ook rekening houden met uitzonderingen zoals parsefouten en het opraken van het opslagquotum.
Voorbeeld van exception-afhandeling bij JSON-parsing
Hier bieden we een wrapper om lezen en schrijven veilig te maken en om JSON.parse-fouten en setItem-exceptions af te handelen.
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}- Je kunt deze utility overal in de app hergebruiken. Voeg een extra wrapper toe wanneer je
try/catchwilt toevoegen.
Voorbeeld van opslaan met TTL (verval)
Omdat Web Storage zelf geen TTL heeft, beheer je dit door een expiresAt aan de waarde toe te voegen.
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 is effectief voor cache en automatisch opslaan (concepten) en kan inconsistenties verminderen.
Voorbeeld van gecentraliseerd sleutelbeheer en namespaces (conflicten vermijden)
Het standaardiseren van sleutels als prefix + versie + naam vermindert conflicten en vereenvoudigt migraties.
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};- Een namespace in de sleutel opnemen maakt het eenvoudiger om van versie te wisselen of later op te schonen.
Voorbeeld van overschreden quotum (QuotaExceededError) en terugvalstrategieën
Houd er rekening mee dat een QuotaExceededError kan optreden bij het uitvoeren van setItem, en ontwerp een fallbackstrategie voor wanneer het opslaan van gegevens mislukt. Wanneer je bijvoorbeeld de opslagcapaciteit overschrijdt, kun je oude gegevens verwijderen of terugvallen op sessionStorage of een in-memory cache om de algehele stabiliteit van de app te behouden.
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");- Afhankelijk van de gekozen fallback is gegevenspersistentie mogelijk niet gegarandeerd. Kies daarom een geschikte opslaglocatie op basis van je use-case. Bijvoorbeeld, in de privénavigatiemodus of onder opslaglimieten kun je de functionaliteit behouden door tijdelijk geheugen of
sessionStoragete gebruiken.
Voorbeeld van synchronisatie tussen tabs (het storage-event) en meldingen binnen dezelfde tab
Met window.addEventListener('storage', …) kun je opslagwijzigingen detecteren die in andere tabbladen hebben plaatsgevonden. Dit event wordt echter niet geactiveerd binnen hetzelfde tabblad. Publiceer daarom voor wijzigingsmeldingen binnen hetzelfde tabblad je eigen events met 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});- Met deze code kun je opslagwijzigingen synchroniseren in zowel andere tabbladen als het huidige tabblad.
Typeveilige registry (strikte typing per sleutel)
Definieer een mapping van sleutel naar type in TypeScript om fouten bij het opslaan en ophalen te voorkomen.
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 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}
Serialisatie/reviver voor complexe types (Date/Map, enz.)
Gebruik de replacer en reviver van JSON.stringify om objecten zoals Date en Map correct te herstellen.
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}- Met deze methode worden
Date-objecten correct hersteld alsDate. Op dezelfde manier kunnenMapenSetook worden gemarkeerd en hersteld.
Voorbeeld van versiebeheer en migratiestrategieën
Als je het opslagformaat in de toekomst mogelijk wijzigt, neem dan een versie op in de payload en bereid migraties voor.
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}- Door kleine migraties te stapelen kun je achterwaartse compatibiliteit behouden.
Voorbeeld van afhandeling in SSR (server-side rendering)
In omgevingen zonder window zal direct verwijzen naar localStorage tot een crash leiden, gebruik dus omgevingscontroles.
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");- Voor code die SSR moet ondersteunen: vergeet niet altijd
typeof windowte controleren.
Praktische tips
- Beperk de schrijffrequentie met debounce/throttle (om pieken door UI-acties te dempen).
- Beheer sleutels met een namespace + versie, bijv.
app:v1:.... - Sla als regel geen gevoelige informatie op (access tokens, enz.). Als het toch moet, overweeg dan korte levensduur plus servervalidatie of WebCrypto.
- Capaciteit hangt af van de browser (enkele MB), dus sla grote gegevens op in IndexedDB.
- Gebruik
CustomEventvoor meldingen in dezelfde tab enstoragevoor tussen tabs. - Controleer in SSR altijd
typeof window.
Geconsolideerde 'type-safe store'-klasse
Laten we kijken naar een voorbeeldimplementatie van een generieke klasse die de elementen integreert die we tot nu toe hebben behandeld, waaronder namespaces, typeveiligheid, vervaltijd (TTL) en exceptieafhandeling. Overweeg in echte producten om tests, logging, LRU-gebaseerde verwijdering van oude gegevens, encryptie en meer toe te voegen.
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}- Deze code toont de configuratie en type-definities voor het bouwen van een typeveilige, rijk uitgeruste key-value-opslag.
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; }- Deze code vormt de kern van de klasse
TypedStorage, die typeveilige key-value-opslag biedt. De klasse beheert de toegestane sleutels en hun typen op basis van deregistryen genereert opslagsleutels met een prefix. Verder gebruikt hetlocalStorageen een in-memory fallback, en kun je een eventnaam instellen voor wijzigingsmeldingen binnen hetzelfde tabblad.
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 }- De methode
gethaalt op typeveilige wijze de waarde op voor de opgegeven sleutel en kan desgewenst waarden met een TTL (vervaltijd) afhandelen.
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 }- De methode
setslaat een waarde op onder de opgegeven sleutel en retourneert een boolean die aangeeft of dit is gelukt.
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 }- De methode
setWithTTLslaat een waarde op met een TTL (vervaltijd).
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 }- De methode
removeverwijdert de waarde voor de opgegeven sleutel zowel uit de opslag als uit de 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}- De methode
clearverwijdert alle gegevens die zijn opgeslagen in zowel de opslag als de 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
-
Deze code is een voorbeeld waarin
TypedStoragewordt gebruikt, dat waarden zoals"theme"en"draft"typeveilig opslaat en ophaalt in een key-value-opslag, en ook TTL en een fallback ondersteunt. Het configureert meldingen binnen hetzelfde tabblad en een in-memory fallback om opslagbewerkingen veilig uit te voeren. -
De klasse
TypedStorageis een praktisch startpunt. Implementeer naar behoefte LRU-strategieën, encryptie, compressie en fallbacks naar IndexedDB.
Samenvatting
Houd bij het gebruik van Web Storage met TypeScript altijd vier punten in gedachten—typeveiligheid, robuustheid tegen exceptions, beveiliging en synchronisatie (meerdere tabs)—voor een robuust ontwerp. De wrappers en utilities die we tot nu toe hebben gezien zijn daar voorbeelden van. Je kunt indien nodig ook migreren naar andere browseropslag zoals IndexedDB.
Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.