@jeanharo98/typed-storage-angular
July 24, 2026 · View on GitHub
Angular wrapper for @jeanharo98/typed-storage with native Angular Signals integration and automatic localStorage sync.
@Service()
export class StorageService {
storage: AppStorage;
constructor() {
const ts = new TypedStorageService();
this.storage = ts.initialize({
theme: 'dark' as 'dark' | 'light',
language: 'es' as 'es' | 'en',
fontSize: 16,
}, { prefix: 'app', sync: true }) as unknown as AppStorage;
}
}
// In your template:
// {{ storageService.storage.theme() }} ← native Angular Signal
✨ Features
- Native Angular Signals — every storage key becomes a
Signal<T> - Automatic sync —
onChange()updates Signals when storage changes - Zoneless compatible — works with Angular 22+ zoneless change detection
- Type-safe — full TypeScript support with your own interfaces
trackRoute()— automatic route-based value sync via Angular Router- All typed-storage features — TTL, cross-tab sync, prefix, sessionStorage, MemoryStorage fallback,
destroy(),batch(),archive()/restore(),routeOverrides
📦 Installation
npm install @jeanharo98/typed-storage @jeanharo98/typed-storage-angular
# or
pnpm add @jeanharo98/typed-storage @jeanharo98/typed-storage-angular
Both packages are required —
@jeanharo98/typed-storageis a peer dependency.@angular/routeris required only if you usetrackRoute().
🚀 Usage
1. Define your storage interface
import { Signal } from '@angular/core';
interface AppStorage {
theme: Signal<'dark' | 'light'>;
language: Signal<'es' | 'en'>;
fontSize: Signal<number>;
sidebarOpen: Signal<boolean>;
set(key: string, value: any): void;
reset(key: string): void;
remove(key: string): void;
has(key: string): boolean;
clear(): void;
destroy(): void;
setRoute(route: string): void;
batch(values: Partial<{ theme: 'dark' | 'light'; language: 'es' | 'en'; fontSize: number; sidebarOpen: boolean }>): void;
archive(): Promise<void>;
restore(): Promise<void>;
}
2. Create a service
import { Service } from '@angular/core';
import { TypedStorageService } from '@jeanharo98/typed-storage-angular';
import { Signal } from '@angular/core';
@Service()
export class StorageService {
storage: AppStorage;
constructor() {
const ts = new TypedStorageService();
this.storage = ts.initialize({
theme: 'dark' as 'dark' | 'light',
language: 'es' as 'es' | 'en',
fontSize: 16,
sidebarOpen: true,
}, {
prefix: 'app', // keys stored as 'app:theme', 'app:language', etc.
sync: true // sync across browser tabs
}) as unknown as AppStorage;
}
}
3. Use in components
import { Component, inject } from '@angular/core';
import { StorageService } from './storage.service';
@Component({
selector: 'app-root',
template: `
<p>Theme: {{ storageService.storage.theme() }}</p>
<p>Language: {{ storageService.storage.language() }}</p>
<p>FontSize: {{ storageService.storage.fontSize() }}</p>
<p>¿Tiene theme? {{ storageService.storage.has('theme') }}</p>
<button (click)="storageService.storage.set('theme', 'light')">Light</button>
<button (click)="storageService.storage.set('theme', 'dark')">Dark</button>
<button (click)="storageService.storage.reset('theme')">Reset</button>
<button (click)="storageService.storage.remove('theme')">Remove Theme</button>
<button (click)="storageService.storage.clear()">Clear All</button>
`
})
export class App {
storageService = inject(StorageService);
}
📦 Batch updates
Update multiple keys in a single call, syncing only the affected Signals:
@Component({
template: `
<button (click)="save()">Save preferences</button>
`
})
export class SettingsComponent {
storageService = inject(StorageService);
save(): void {
this.storageService.storage.batch({
theme: 'light',
fontSize: 20
// sidebarOpen is not included — stays unchanged
});
}
}
See the typed-storage README for the full documentation.
📦 Archiving to IndexedDB with archive() and restore()
Both methods are async — free real space in localStorage while a page isn't in active use, and bring the data back when needed:
@Component({ selector: 'app-editor' })
export class EditorComponent implements OnDestroy {
storageService = inject(StorageService);
async ngOnInit(): Promise<void> {
await this.storageService.storage.restore();
// brings back any previously archived data for this schema
}
async ngOnDestroy(): Promise<void> {
await this.storageService.storage.archive();
// moves current data to IndexedDB, frees localStorage
}
}
Signals update automatically after both calls resolve — archive() resets them to their initialValue (since the data moved out of localStorage), and restore() updates them with whatever was brought back. See the typed-storage README for the full documentation and how this differs from the automatic quota-exceeded fallback.
🗑️ Scoped storage with destroy()
Same as the core library — completely removes all schema keys, useful for data that should only exist while a specific page/component is active:
@Component({ selector: 'app-products' })
export class ProductsComponent implements OnDestroy {
storageService = inject(StorageService);
ngOnDestroy(): void {
this.storageService.storage.destroy();
// → all keys removed from localStorage when leaving this component
}
}
See the typed-storage README for when to use destroy() vs ttl.
🧭 Route-based values with trackRoute()
If your schema uses routeOverrides, connect it to Angular Router automatically — no manual subscription needed:
import { Router } from '@angular/router';
import { TypedStorageService, trackRoute } from '@jeanharo98/typed-storage-angular';
@Service()
export class StorageService {
storage: AppStorage;
constructor(private router: Router) {
const ts = new TypedStorageService();
this.storage = ts.initialize({
theme: 'dark' as 'dark' | 'light',
}, {
prefix: 'app',
routeOverrides: {
'/': { theme: 'dark' },
'/about': { theme: 'light' }
}
}) as unknown as AppStorage;
trackRoute(this.storage, this.router);
// Now navigating to /about automatically sets theme to 'light',
// and navigating to / sets it back to 'dark' — no manual setRoute() calls
}
}
trackRoute() subscribes to router.events, filters for NavigationEnd, and calls storage.setRoute(event.urlAfterRedirects) on every navigation. See the typed-storage README for the full routeOverrides documentation, including how to remove a key entirely for a specific route using null, and how to apply an override only once with __once.
🧩 Independent storage per page (separate prefix, no trackRoute())
routeOverrides (with or without __once) always operates on one shared value across your whole app — it never gives two pages their own truly independent copies of a key. If you instead want HomeComponent and AboutComponent to each keep their own theme, completely unaffected by each other, use a separate TypedStorageService (or a separate prefix) per page instead:
// home-storage.ts — its own isolated service
@Service()
export class HomeStorageService {
storage: { theme: Signal<'dark' | 'light'>; set(key: string, value: any): void; destroy(): void; };
constructor() {
const ts = new TypedStorageService();
this.storage = ts.initialize({
theme: 'dark' as 'dark' | 'light'
}, { prefix: 'home' }) as any; // stored as 'home:theme'
}
}
// about-storage.ts — a completely separate isolated service
@Service()
export class AboutStorageService {
storage: { theme: Signal<'dark' | 'light'>; set(key: string, value: any): void; };
constructor() {
const ts = new TypedStorageService();
this.storage = ts.initialize({
theme: 'light' as 'dark' | 'light'
}, { prefix: 'about' }) as any; // stored as 'about:theme'
}
}
HomeComponent injects HomeStorageService, AboutComponent injects AboutStorageService — changing one's theme never touches the other's, because they're two entirely different localStorage keys (home:theme and about:theme). No routeOverrides, no trackRoute(), no __once needed for this — it's the right tool when true per-page isolation is what you want.
See the typed-storage README's pattern comparison table for a full breakdown of when to use separate prefixes vs. routeOverrides (with or without __once).
⚙️ Options
All options from @jeanharo98/typed-storage are supported:
ts.initialize(schema, {
prefix: 'myapp', // Prefix keys — 'myapp:theme'
storage: 'session', // Use sessionStorage instead of localStorage
ttl: 3600000, // Expire after 1 hour
sync: true, // Sync across browser tabs
routeOverrides: { // Different values per route
'/checkout': { currency: null }
},
validate: {}, // Optional Zod-compatible runtime validation
plugins: [], // Optional lifecycle hooks
encrypt: true, // Requires 'secret' — see typed-storage docs for security notes
})
initialize()'s second parameter is entirely optional — it accepts the exact same options as createStorage() in the core library. See the typed-storage README for the full list of options.
🔔 How it works
TypedStorageService.initialize()
│
├── createStorage(schema, options) ← from @jeanharo98/typed-storage
│
├── For each key in schema:
│ ├── signal(storage[key]()) ← creates Angular Signal with initial value
│ └── storage[key].onChange() ← connects storage changes to Signal
│
└── Returns object with:
├── theme() ← Signal getter
├── language() ← Signal getter
├── set(key, value) ← updates storage + Signal
├── reset(key) ← resets to initialValue + updates Signal
├── remove(key) ← removes key from storage + sets Signal to undefined
├── has(key) ← checks if key exists in storage
├── clear() ← clears all keys + updates all Signals
├── destroy() ← removes ALL keys completely + updates all Signals
├── setRoute(route) ← applies routeOverrides for that route + updates Signals
├── batch(values) ← updates multiple keys + syncs only affected Signals
├── archive() ← moves data to IndexedDB + syncs Signals (async)
└── restore() ← brings data back from IndexedDB + syncs Signals (async)
destroy() and setRoute() don't need manual Signal syncing for their underlying .set()/.remove() calls — those already have onChange wired to the Angular Signals from step 2. batch(), archive(), and restore() explicitly re-sync their affected Signals after calling the core methods, since they operate on the core storage directly rather than going through the per-key set() wrapper.
📋 API Reference
TypedStorageService
initialize(schema, options?)
Creates a storage object with Angular Signals.
| Parameter | Type | Description |
|---|---|---|
schema | StorageSchema | Object with keys and initial values |
options | StorageSignalOptions | Optional configuration |
Returns an object where each key is a Signal<T>, plus the following methods:
Methods
| Method | Description |
|---|---|
set(key, value) | Updates the value in storage and syncs the Signal |
reset(key) | Resets to initialValue in storage and syncs the Signal |
remove(key) | Removes the key from localStorage and sets Signal to undefined |
has(key) | Returns true if the key exists in storage |
clear() | Calls reset() on all keys and syncs all Signals |
destroy() | Completely removes all keys and syncs all Signals |
setRoute(route) | Applies the routeOverrides entry for route, if any, and syncs Signals |
batch(values) | Updates multiple keys in a single call, syncing only the affected Signals |
archive() | Moves all schema keys to IndexedDB, removes them from localStorage, syncs Signals. Async |
restore() | Brings archived keys back from IndexedDB into localStorage, syncs Signals. Async |
Difference between reset(), remove(), clear() and destroy()
// reset(key) — vuelve al initialValue pero mantiene la key en localStorage
storage.reset('theme');
// localStorage['app:theme'] = '"dark"' — sigue existiendo
// storage.theme() → 'dark' (initialValue)
// remove(key) — borra la key del localStorage completamente
storage.remove('theme');
// localStorage['app:theme'] — ya no existe
// storage.theme() → undefined
// clear() — reset() en todas las keys del schema
storage.clear();
// todas las keys vuelven a su initialValue, pero SIGUEN existiendo en localStorage
// destroy() — remove() en todas las keys del schema
storage.destroy();
// todas las keys DESAPARECEN completamente de localStorage
trackRoute(storage, router)
Subscribes storage.setRoute() to Angular Router navigation events automatically.
| Parameter | Type | Description |
|---|---|---|
storage | { setRoute(route: string): void } | Any object exposing setRoute() — normally the result of initialize() |
router | Router | An injected instance of Angular's Router |
🆚 Comparison
| Feature | Manual approach | typed-storage-angular |
|---|---|---|
| localStorage persistence | ✅ manual | ✅ automatic |
| Angular Signal | ✅ manual signal() | ✅ automatic |
| Cross-tab sync | ❌ manual StorageEvent | ✅ automatic |
| TTL / expiration | ❌ manual | ✅ built-in |
| Route-based values | ❌ manual router subscription | ✅ trackRoute() |
| Batch updates | ❌ manual, one .set() per key | ✅ batch() |
| Large data archiving | ❌ manual IndexedDB code | ✅ archive() / restore() |
| Type safety | ⚠️ partial | ✅ full |
| Prefix namespacing | ❌ manual | ✅ built-in |
| Lines of code (4 keys) | ~25 lines | ~5 lines |
🔗 Related
- @jeanharo98/typed-storage — Core library (required peer dependency)
- @jeanharo98/typed-storage-react — React wrapper
- typed-storage-devtools — Chrome DevTools extension for real-time inspection
📄 License
MIT