TypeScript-first wrapper around localStorage that provides typed keys, schema validation, and automatic data migrations — so persisted client-side data never breaks your app.
npm install schema-storage zodimport { createStorage } from "schema-storage";
import { z } from "zod";
const storage = createStorage({
user: z.object({
id: z.string(),
email: z.string().email(),
}),
theme: z.enum(["light", "dark"]),
});
// Fully typed!
storage.set("theme", "dark"); // ✅
storage.set("theme", "blue"); // ❌ Type error
const user = storage.get("user"); // { id: string; email: string } | null
const theme = storage.getOrDefault("theme", "light"); // "light" | "dark"No more magic strings. All keys are typed and autocompleted.
const storage = createStorage({
settings: z.object({
language: z.string(),
notifications: z.boolean(),
}),
});
storage.get("settings"); // Typed return
storage.get("unknown"); // ❌ Type errorEvery read operation validates data against your schema. Invalid or corrupted data is safely rejected.
const storage = createStorage({
count: z.number(),
}, {
onInvalid: "reset", // or "throw" | "ignore"
});
// If localStorage contains invalid JSON or wrong shape:
const count = storage.get("count"); // null (or throws if onInvalid: "throw")Safely evolve your data models without breaking existing users.
const storage = createStorage({
user: {
version: 2,
schema: z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
}),
migrate: {
1: (oldData) => ({
id: oldData.id,
email: "unknown@example.com",
name: "User",
}),
},
},
});When get("user") is called:
- Detects stored version (1)
- Applies migration from 1 → 2
- Validates final result
- Persists upgraded version automatically
Use any storage backend with the same API.
import { createStorage, MemoryStorage } from "schema-storage";
// Default: localStorage
const storage1 = createStorage({ ... });
// sessionStorage
const storage2 = createStorage({ ... }, { driver: sessionStorage });
// Memory (SSR-safe, testing)
const storage3 = createStorage({ ... }, { driver: new MemoryStorage() });Creates a typed storage instance.
Parameters:
schema: Object mapping keys to Zod schemas or versioned schemasoptions:driver?: StorageDriver- Storage backend (default: localStorage)onInvalid?: "throw" | "reset" | "ignore"- Behavior for invalid data (default: "ignore")prefix?: string- Key prefix for namespacing
Returns: SafeStorage<T>
Reads and validates a value. Returns null if missing or invalid.
Writes a value with validation. Throws if validation fails and onInvalid: "throw".
Removes a key and its metadata.
Clears all storage (or all prefixed keys if using a prefix).
Checks if a key exists.
Gets a value or returns the default if missing/invalid.
import { createStorage } from "schema-storage";
import { z } from "zod";
const storage = createStorage({
todos: z.array(z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
})),
filter: z.enum(["all", "active", "completed"]),
});
storage.set("todos", [
{ id: "1", text: "Learn schema-storage", completed: false },
]);
const todos = storage.get("todos"); // Fully typed!const storage = createStorage({
preferences: {
version: 3,
schema: z.object({
theme: z.enum(["light", "dark", "auto"]),
fontSize: z.number().min(12).max(24),
language: z.string(),
}),
migrate: {
1: (old) => ({
theme: old.theme ?? "light",
fontSize: 16,
language: "en",
}),
2: (old) => ({
...old,
theme: old.theme === "system" ? "auto" : old.theme,
}),
},
},
});import { createStorage, type StorageDriver } from "schema-storage";
class CustomStorage implements StorageDriver {
private data = new Map<string, string>();
getItem(key: string): string | null {
return this.data.get(key) ?? null;
}
setItem(key: string, value: string): void {
this.data.set(key, value);
}
removeItem(key: string): void {
this.data.delete(key);
}
clear(): void {
this.data.clear();
}
key(index: number): string | null {
return Array.from(this.data.keys())[index] ?? null;
}
get length(): number {
return this.data.size;
}
}
const storage = createStorage({ ... }, { driver: new CustomStorage() });localStorage problems:
- Everything is a string (manual JSON parsing)
- No type safety
- No validation
- No migration strategy
schema-storage solves:
- ✅ Typed keys with autocomplete
- ✅ Automatic JSON handling
- ✅ Runtime validation with Zod
- ✅ Versioned migrations
- ✅ Storage-agnostic design
MIT