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

Walkthrough

Space play step click any line
Three takeaways
  1. 1A generic type parameter constrained to a schema lets one class enforce per-key value types across every method.
  2. 2Wrapping a stringly-typed browser API in JSON serialization plus try/catch turns fragile storage access into a safe, self-cleaning interface.
  3. 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.

A type-safe wrapper around localStorage — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code