typedstorage-safe
v1.0.1
Published
TypeScript-first localStorage and sessionStorage with TTL expiry, SSR safety, cross-tab sync, and schema types. Zero dependencies.
Downloads
126
Maintainers
Readme
typedstorage-safe
TypeScript-first localStorage and sessionStorage with TTL expiry, SSR safety, cross-tab sync, and full type inference. Zero dependencies. ~1.5KB gzipped.
What's New
1.0.1
- Fix:
nulland falsy values (false,0,'') now round-trip correctly.has()andgetAll()no longer treat a storednullas a missing key. - Fix:
keys()now excludes expired keys, matching its documented "non-expired" contract. - Fix:
onChange()now fires when another tab callsclear()(aStorageEventwithkey === null), and ignores events from a different storage backend (e.g.sessionvslocal). - Fix: corrupt/empty entries are read more robustly (
nullcheck instead of a falsy check). - Docs: added a Security & Limitations section covering web-storage safety,
clear()scope, the in-memory server fallback, storage quota, and schema migrations.
Why typedstorage-safe?
Plain localStorage has four major problems that every developer hits:
| Problem | Plain localStorage | typed-storage |
|---|---|---|
| No TypeScript types | JSON.parse() returns any | Full schema inference |
| No expiry/TTL | Must hand-roll timestamp logic | Built-in '7d', '1h', '30s' |
| Crashes in SSR / Next.js | ReferenceError: localStorage is not defined | Silently returns null |
| Key collisions | Global namespace, easy to clash | Prefix namespacing |
Installation
npm install typedstorage-safeyarn add typedstorage-safepnpm add typedstorage-safeQuick Start
import { createStorage } from 'typedstorage-safe'
const store = createStorage({ prefix: 'myapp' })
store.set('theme', 'dark')
store.get('theme') // 'dark'
store.remove('theme')TypeScript Schema — Full Type Safety
Define your schema once and get full autocomplete and error detection everywhere.
import { createStorage } from 'typedstorage-safe'
type AppSchema = {
user: { id: number; name: string; role: 'admin' | 'user' }
theme: 'light' | 'dark' | 'system'
token: string
cart: Array<{ id: string; qty: number; price: number }>
onboardingStep: 1 | 2 | 3 | 4
}
const store = createStorage<AppSchema>({ prefix: 'app' })
// ✅ TypeScript knows exactly what each key holds
store.set('theme', 'dark')
store.set('theme', 'purple') // ❌ TypeScript ERROR
store.get('user')?.role // typed as 'admin' | 'user'
store.get('cart')?.[0].price // typed as number
store.set('unknown', 'x') // ❌ TypeScript ERROR — not in schemaTTL — Auto-Expiring Data
Stop writing Date.now() + 7 * 24 * 60 * 60 * 1000 by hand. Use human-readable TTL.
// Shorthand strings
store.set('token', 'abc123', { ttl: '1h' }) // expires in 1 hour
store.set('session', 'xyz', { ttl: '30m' }) // expires in 30 minutes
store.set('otp', '123456', { ttl: '5m' }) // expires in 5 minutes
store.set('cart', [...], { ttl: '7d' }) // expires in 7 days
store.set('flag', true, { ttl: '30s' }) // expires in 30 seconds
// Raw milliseconds also work
store.set('token', 'abc123', { ttl: 3_600_000 })
// Reading an expired key returns null and auto-removes it
store.get('otp') // null after 5 minutesSupported units: s (seconds), m (minutes), h (hours), d (days).
SSR Safe — Works in Next.js App Router
// ✅ No crash in Server Components or getServerSideProps
const theme = store.get('theme') // returns null on server, value on client
// Works in useEffect too
useEffect(() => {
const user = store.get('user')
if (user) setUser(user)
}, [])When localStorage is unavailable (SSR, Edge, private browsing), typed-storage automatically falls back to an in-memory store so your code never throws.
Prefix Namespacing — No Key Collisions
const authStore = createStorage<AuthSchema>({ prefix: 'auth' })
const uiStore = createStorage<UISchema> ({ prefix: 'ui' })
authStore.set('token', 'abc') // stored as "auth:token"
uiStore.set('token', 'xyz') // stored as "ui:token" — no conflict
authStore.clear() // clears only "auth:*" keys, leaves "ui:*" untouchedCross-Tab Sync
Get notified when a value changes in another browser tab.
const store = createStorage<AppSchema>({ prefix: 'app', sync: true })
// Listen for changes from other tabs
const unsubscribe = store.onChange('user', (newValue) => {
console.log('User updated in another tab:', newValue)
setUser(newValue)
})
// Clean up when component unmounts
onUnmount(unsubscribe)Full API Reference
createStorage<Schema>(options?)
Creates a typed storage instance.
const store = createStorage<Schema>({
prefix: 'myapp', // string — key prefix. Default: ''
storage: 'local', // 'local' | 'session' | 'memory'. Default: 'local'
sync: false, // enable cross-tab sync. Default: false
})store.set(key, value, options?)
Write a value.
store.set('theme', 'dark')
store.set('token', 'abc', { ttl: '1h' })store.get(key)
Read a value. Returns null if missing or expired.
const theme = store.get('theme') // 'light' | 'dark' | 'system' | nullstore.remove(key)
Delete a key.
store.remove('token')store.has(key)
Check if a key exists and is not expired.
if (store.has('token')) {
// token exists and is still valid
}store.clear()
Remove all keys belonging to this store's prefix.
store.clear()store.keys()
List all keys (without prefix, non-expired).
store.keys() // ['theme', 'token', 'cart']store.ttl(key)
Get remaining TTL in milliseconds, or null if no TTL set.
store.set('token', 'abc', { ttl: '1h' })
store.ttl('token') // ~3_600_000 msstore.getAll()
Get all non-expired values as an object.
store.getAll() // { theme: 'dark', token: 'abc123' }store.onChange(key, callback)
Listen for changes from other tabs (requires sync: true). Returns an unsubscribe function.
const unsub = store.onChange('user', (value) => console.log(value))
unsub() // stop listeningFramework Examples
React
import { createStorage } from 'typedstorage-safe'
import { useState, useEffect } from 'react'
type Schema = { theme: 'light' | 'dark' }
const store = createStorage<Schema>({ prefix: 'app' })
function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>(() =>
store.get('theme') ?? 'light'
)
function toggle() {
const next = theme === 'light' ? 'dark' : 'light'
store.set('theme', next)
setTheme(next)
}
return { theme, toggle }
}Next.js App Router
// lib/storage.ts — shared across the app
import { createStorage } from 'typedstorage-safe'
type AppSchema = {
user: { id: string; name: string }
theme: 'light' | 'dark'
}
export const appStore = createStorage<AppSchema>({
prefix: 'myapp',
sync: true,
})// app/layout.tsx — server component, no crash
import { appStore } from '@/lib/storage'
export default function Layout({ children }) {
const theme = appStore.get('theme') // null on server — safe
return <html data-theme={theme ?? 'light'}>{children}</html>
}Vue 3
import { createStorage } from 'typedstorage-safe'
import { ref, watchEffect } from 'vue'
type Schema = { theme: 'light' | 'dark' }
const store = createStorage<Schema>({ prefix: 'app' })
export function useTheme() {
const theme = ref(store.get('theme') ?? 'light')
watchEffect(() => {
store.set('theme', theme.value)
})
return { theme }
}Vanilla TypeScript / Node.js
import { createStorage } from 'typedstorage-safe'
const store = createStorage({ storage: 'memory' }) // in-memory for Node.js
store.set('key', 'value', { ttl: '5m' })
store.get('key') // 'value'Migration from Plain localStorage
// Before
localStorage.setItem('theme', JSON.stringify('dark'))
const theme = JSON.parse(localStorage.getItem('theme') ?? 'null')
// After
const store = createStorage<{ theme: 'light' | 'dark' }>({ prefix: 'app' })
store.set('theme', 'dark')
const theme = store.get('theme')Bundle Size
- ~1.5 KB gzipped
- Zero runtime dependencies
- Tree-shakeable ESM build
Tested with: React 18, Next.js 13/14/15, Vue 3, Svelte, plain TypeScript, Node.js 18+.
Security & Limitations
typedstorage-safe is a convenience wrapper around the browser's Storage API. It is not a security boundary. Please read these caveats before storing anything sensitive.
⚠️ Do not store secrets in localStorage / sessionStorage
Everything written to localStorage or sessionStorage is stored in plaintext and is readable by any JavaScript running on your page — including third-party scripts and anything injected via XSS. This applies to token, session, and otp-style values shown in some examples above.
- Auth tokens are best kept in memory or in a
Secure,HttpOnlycookie set by your server — not in web storage. - TTL is a client-side convenience, not a security control. An expired value is only purged when it is next read (
get,has,keys,getAll). Until then the raw bytes remain in storage, and nothing on the server enforces the expiry.
clear() without a prefix wipes the entire origin
For a store created without a prefix, clear() calls the underlying Storage.clear(), which removes all keys for the current origin — including data written by other libraries or other parts of your app. To scope clearing to your own data, always create stores with a prefix:
const store = createStorage<Schema>({ prefix: 'myapp' })
store.clear() // only removes "myapp:*" keysThe in-memory fallback is process-global (server use)
When storage is unavailable (SSR, Node.js, Edge, private browsing), the library falls back to a single in-memory Map that is shared across the whole process. In a long-running server this means state is not isolated between requests or users — treat storage: 'memory' as a global cache, and never put per-user data in it on the server.
set() can throw when storage is full
If the browser's storage quota is exceeded (or writes are blocked), adapter.setItem() throws a QuotaExceededError. set() does not currently swallow this, so wrap writes you can't guarantee will fit:
try {
store.set('cart', bigCart)
} catch (err) {
// storage full — fall back gracefully
}No schema migrations
Stored values are not versioned or re-validated on read. If you change a key's type in your Schema, previously stored data is returned as-is with the new type — TypeScript will trust it, but the runtime shape may not match. Bump your prefix (e.g. myapp.v2) or clear old keys when you change shapes.
License
MIT © Mehulbirare
