@dmytromykhailiuk/typed-local-storage
v1.0.0
Published
Typed localStorage with initial values, groups and cross-tab subscriptions. SSR-safe, dependency-free, a few hundred bytes.
Downloads
85
Maintainers
Readme
@dmytromykhailiuk/typed-local-storage
Typed localStorage with initial values, groups and cross-tab subscriptions.
SSR-safe, dependency-free, a few hundred bytes.
Full documentation: open Docs in a browser — every option, with examples, a table of contents and cross-links. This README is the short form.
⚠️ The rule that makes it work: never touch the raw
localStorageobject once its keys are owned by storages — and never calllocalStorage.clear(). A directsetItembypasses typing, serialization and this tab's subscribers;localStorage.clear()levels the whole origin, including keys that had to survive. Every operation has a safe counterpart:set/updateto write,storage.clear()to reset one storage,group.clear()to reset a related slice.
Built for apps that keep a lot in localStorage — settings, drafts, filters, tokens,
per-feature caches. At that scale two things start to hurt: typing, because every read is an
untyped string you have to parse and trust; and management, because clearing state means
either hunting down keys one by one or reaching for localStorage.clear() — which wipes the
whole origin, including what had to survive.
This library answers both. Each storage is declared once, owns one key, and carries its
value type in its signature — no string keys scattered around the codebase, no
JSON.parse(localStorage.getItem(...) ?? "null") ceremony, no silent crashes on corrupted data.
And groups make cleanup targeted: related storages are cleared together, in one call,
each by its own rules — everything else stays untouched.
Install
npm i @dmytromykhailiuk/typed-local-storageQuick start
import { createLocalStorage } from "@dmytromykhailiuk/typed-local-storage";
const settings = createLocalStorage("app:settings", {
initialValue: { theme: "dark", fontSize: 14 },
});
settings.get(); // { theme: "dark", fontSize: 14 } — typed
settings.set({ theme: "light", fontSize: 16 });
settings.update((s) => ({ ...s, fontSize: s.fontSize + 1 }));
settings.clear(); // back to the initial value
const unsubscribe = settings.subscribe((value) => {
// fires on every write through this instance — and when another tab writes the key
});- The value type is inferred from
initialValue(or passed explicitly:createLocalStorage<Session>("app:session")). - With an
initialValue,get()returnsT; without one it returnsT | undefined— the types follow. - The initial value is written to storage at creation only when the key is absent — a stored
0,falseor""is a value, not an absence, and is never overwritten.
API
const storage = createLocalStorage<T>(key, {
initialValue?: T; // seeded when absent; restored by clear(); fallback for get()
isString?: boolean; // store raw, without JSON (inferred for string initial values)
groups?: LocalStoragesGroup[];
onParseError?: (error, raw) => void; // corrupted JSON hook; defaults to console.warn
});
storage.get(); // T (with initialValue) or T | undefined
storage.set(value);
storage.update((v) => next);
storage.clear(); // reset to initialValue, or remove the key when there is none
storage.hasValue(); // does the key exist right now?
storage.subscribe(fn); // local writes + other tabs' writes; returns unsubscribe
storage.key; // the underlying key
storage.initialValue;Registering the same key twice throws — two storages writing one key with different types is a bug worth failing loudly on.
Safety
- Corrupted data never throws. If the stored string is not valid JSON,
get()reports it throughonParseErrorand falls back toinitialValue(orundefined). - SSR-safe. Where
localStorageis missing or throws (Node, sandboxed iframes, disabled cookies), the same API runs against a shared in-memory fallback — pages render, nothing persists, no guards needed in your code. - String mode. With
isString(inferred wheninitialValueis a string) values are stored raw —"dark", not"\"dark\""— which keeps keys readable and compatible with code that wrote them before this library.
Cross-tab subscriptions
subscribe listens to writes made through the instance and to the browser's storage
event, so a change made in another tab lands in the same callback:
const theme = createLocalStorage("app:theme", { initialValue: "dark" });
theme.subscribe((value) => document.body.dataset.theme = value);
// another tab: theme.set("light") → this tab's callback fires with "light"The storage event only fires in other tabs, so a write is delivered exactly once everywhere.
Groups
When an app stores many keys, "clear the user's data" has two bad answers:
localStorage.clear(), which levels the whole origin — theme, language, consent flags,
everything that should have survived — and clearing keys one by one, a list that silently drifts
out of date every time a feature adds a key. A group is the middle ground: storages that belong
together are declared together, and reset together with one call — nothing outside the group is
touched. The classic case — "clear everything user-scoped on logout":
import { createLocalStorage, createLocalStoragesGroup } from "@dmytromykhailiuk/typed-local-storage";
const userScoped = createLocalStoragesGroup("app:user-scoped");
const session = createLocalStorage<Session>("app:session", { groups: [userScoped] });
const drafts = createLocalStorage("app:drafts", { initialValue: [], groups: [userScoped] });
userScoped.clear(); // session removed, drafts reset to []The group persists its member list (key + initial value) in storage under its own name, so
clear() also covers keys registered by previous sessions — code paths that didn't run
this time can't leak stale data. Members with live instances are cleared through them, so their
subscribers are notified.
TypeScript
const counter = createLocalStorage("counter", { initialValue: 0 });
counter.get(); // number — no undefined
counter.update((v) => v + 1); // v: number
const session = createLocalStorage<Session>("session");
session.get(); // Session | undefined
session.update((v) => v ?? emptySession); // v: Session | undefinedLicense
MIT
