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 { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 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.