@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 synconChange() 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-storage is a peer dependency. @angular/router is required only if you use trackRoute().


🚀 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.

ParameterTypeDescription
schemaStorageSchemaObject with keys and initial values
optionsStorageSignalOptionsOptional configuration

Returns an object where each key is a Signal<T>, plus the following methods:

Methods

MethodDescription
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.

ParameterTypeDescription
storage{ setRoute(route: string): void }Any object exposing setRoute() — normally the result of initialize()
routerRouterAn injected instance of Angular's Router

🆚 Comparison

FeatureManual approachtyped-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 subscriptiontrackRoute()
Batch updates❌ manual, one .set() per keybatch()
Large data archiving❌ manual IndexedDB codearchive() / restore()
Type safety⚠️ partial✅ full
Prefix namespacing❌ manual✅ built-in
Lines of code (4 keys)~25 lines~5 lines


📄 License

MIT