typescript
54 lines · 9 steps
A type-safe wrapper around localStorage
A generic class maps a schema type onto the untyped Storage API so every key and value stays checked at compile time.
Explained by
highlit
1type StorageSchema = Record<string, unknown>;
2
3interface StorageOptions {
4 namespace?: string;
5 storage?: Storage;
6}
7
8class TypedStorage<T extends StorageSchema> {
9 private readonly prefix: string;
10 private readonly store: Storage;
11
12 constructor(options: StorageOptions = {}) {
13 this.prefix = options.namespace ? `${options.namespace}:` : "";
14 this.store = options.storage ?? window.localStorage;
15 }
16
17 private keyFor(key: keyof T): string {
18 return `${this.prefix}${String(key)}`;
19 }
20
21 get<K extends keyof T>(key: K): T[K] | null {
22 const raw = this.store.getItem(this.keyFor(key));
23 if (raw === null) return null;
24 try {
25 return JSON.parse(raw) as T[K];
26 } catch {
27 this.store.removeItem(this.keyFor(key));
28 return null;
29 }
30 }
31
32 getOr<K extends keyof T>(key: K, fallback: T[K]): T[K] {
33 return this.get(key) ?? fallback;
34 }
35
36 set<K extends keyof T>(key: K, value: T[K]): void {
37 this.store.setItem(this.keyFor(key), JSON.stringify(value));
38 }
39
40 update<K extends keyof T>(key: K, updater: (current: T[K] | null) => T[K]): void {
41 this.set(key, updater(this.get(key)));
42 }
43
44 remove(key: keyof T): void {
45 this.store.removeItem(this.keyFor(key));
46 }
47
48 clear(): void {
49 for (let i = this.store.length - 1; i >= 0; i--) {
50 const k = this.store.key(i);
51 if (k && k.startsWith(this.prefix)) this.store.removeItem(k);
52 }
53 }
54}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A generic type parameter constrained to a schema lets one class enforce per-key value types across every method.
- 2Wrapping a stringly-typed browser API in JSON serialization plus try/catch turns fragile storage access into a safe, self-cleaning interface.
- 3A key prefix scopes reads and writes to a namespace so multiple stores can share the same underlying Storage without collisions.
Related explainers
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpHandler,
Refreshing tokens with an Angular HttpInterceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
ruby
class Credentials SENSITIVE = %i[password api_key secret_token access_key].freeze REDACTED = "[REDACTED]".freeze
Redacting secrets in Ruby object output
serialization
introspection
security
Intermediate
7 steps
typescript
type Fetcher<T> = (key: string) => Promise<T>; export class RequestDeduplicator<T> { private inFlight = new Map<string, Promise<T>>();
Deduplicating in-flight requests in TypeScript
deduplication
promises
caching
Intermediate
7 steps
typescript
import { Injectable, computed, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs';
Optimistic updates with Angular signals
signals
optimistic-updates
state-management
Intermediate
9 steps
typescript
type EventMap = Record<string, unknown>; type Handler<T> = (payload: T) => void;
A type-safe event bus in TypeScript
generics
event-emitter
type-safety
Advanced
7 steps
python
from datetime import datetime, date from decimal import Decimal from flask import Flask, jsonify from flask.json.provider import DefaultJSONProvider
Serializing SQLAlchemy models in Flask JSON
serialization
orm
json
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-type-safe-wrapper-around-localstorage-explained-typescript-c6f3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.