73-star gem turns chrome.storage from a type-unsafe mess into something actually usable.
The Problem Nobody Talks About Loudly Enough
Every browser extension developer has written this line at least once:
ts
const storage = await chrome.storage.local.get('user-options');
const value = storage['user-options']; // type: any. Great.
And every time, they've felt a small piece of their soul leave. The native chrome.storage API is one of those browser platform decisions that made sense in 2012 and has been silently punishing developers ever since. You get back an object keyed by string, typed as any, with no ergonomic way to listen for changes on a specific key rather than the entire storage universe.
With Manifest V3 becoming mandatory and extension development seeing a genuine renaissance â solo builders are shipping full-blown AI sidebars, productivity tools, and developer utilities as browser extensions â the pain of the storage API is being felt by more people than ever. That's exactly why webext-storage by Federico Brigante deserves your attention right now.
What It Actually Does
webext-storage is a thin, typed wrapper around chrome.storage that gives you two primary primitives:
StorageItemâ represents a single value in storage, with full TypeScript genericsStorageItemMapâ represents a collection of same-typed values, behaving like aMap
The conceptual shift is important: instead of thinking about storage as a flat key-value bag you query at runtime, you define your storage schema upfront as typed objects. Here's the comparison the README uses, and it's damning:
ts
// webext-storage
const options = new StorageItem<Record<string, string>>('user-options');
const value = await options.get(); // Record<string, string> | undefined
await options.set({color: 'red'}); // type-checked
options.onChanged(newValue => {
console.log('New options', newValue);
});
Versus the native API where you're fishing a value out of an untyped bag, manually filtering chrome.onChanged listeners by storage area AND key name, and hoping you didn't typo the string.
Technical Deep-Dive
The StorageItem class handles three things that the native API fails at:
1. Type narrowing based on default values. If you construct new StorageItem<string>('theme', { defaultValue: 'dark' }), the return type of .get() is string â not string | undefined. The undefined only appears when no default is provided. This is a small thing that removes an enormous amount of defensive code from real extensions.
2. Unsetting via .set(undefined). The native chrome.storage.local.set() silently ignores undefined values â a footgun that has caused countless bugs. StorageItem.set(undefined) actually removes the key, matching developer intent.
3. Per-item change listeners via .onChanged(). Instead of registering a global chrome.storage.onChanged listener and manually checking storageArea and key names inside a callback â which gets messy fast in any extension with more than a handful of storage keys â you attach listeners directly to the item. The library handles the routing internally.
For related grouped values, StorageItemMap gives you map-like semantics: multiple keys with the same value type, managed together. Think storing per-domain settings, or per-tab state.
The bundle is tiny â check the bundlephobia badge in the README, it's minzipped to almost nothing. There's also a standalone bundle available via bundle.fregante.com for people who want to drop it directly into manifest.json without a build step.
Cross-browser support covers Chrome, Firefox, and Safari, works across Manifest V2 and V3, and can be called from any extension context â background, content scripts, popups, options pages.
Honest Limitations
Let's be real about what this library isn't.
It's not a full storage abstraction layer. If you need migrations, schema versioning, or complex relational data, webext-storage won't help you. You'd be combining it with something else or rolling your ow