10/23/2025
Typed Storage - A Type-Safe Local Storage Library
Why Typed Storage?
For some time, I’ve been fascinated by the developer experience offered by type-safe APIs like tRPC. The seamless type completion and compile-time checks for HTTP requests are incredibly powerful. This inspired me to create a similar type-safe API, but for a different domain: localStorage. Thus, Typed Storage was born. It aims to bring the same level of type safety and developer comfort to your client-side data persistence.
How it Works
Typed Storage leverages zod schemas to define the structure and types of your stored data, offering a robust and familiar way to ensure data integrity.
Initiating the Client
The first step to a type-safe experience is defining your data schema. To avoid inventing a new schema language, Typed Storage integrates with zod, a widely adopted and powerful schema validation library.
import { createClient } from "@giftlion/typed-storage";
import { z } from "zod";
// Define your data schema using Zod
const schema = z.object({
users: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
}),
// You can define other top-level keys here
settings: z.object({
theme: z.enum(["light", "dark"]),
notifications: z.boolean().default(true),
}),
});
// Create your Typed Storage client instance
// The first argument is a unique namespace for your storage instance.
const typedStorage = createClient("my-app-storage", { schema });The first argument to createClient is a unique string that acts as a namespace for your storage instance, allowing you to manage multiple distinct storage areas if needed.
CRUD Operations
With the client initialized, you can now perform Create, Read, Update, and Delete (CRUD) operations with full type safety and auto-completion.
// Insert a new user
await typedStorage.users.insert({
id: 1,
name: "Alice",
email: "alice@example.com",
});
// Query all users
const { data: allUsers } = await typedStorage.users.query();
// Query users with a filter (e.g., id greater than 10)
const { data: activeUsers } = await typedStorage.users
.query()
.where((user) => user.id > 10);
console.log("Active Users (ID > 10):", activeUsers);
// Query users and select only specific fields (e.g., email)
const { data: userEmails } = await typedStorage.users
.query()
.select({ email: true });
// type userEmails = { email:string }[]
// Chain 'where' and 'select' for more complex queries
const { data: filteredNamesAndEmails } = await typedStorage.users
.query()
.where((user) => user.name.includes("Alice"))
.select({ name: true, email: true });
// type filteredNamesAndEmails = { name: true, email: true }[]
// Update a user's name
await typedStorage.users
.update()
.where((user) => user.id === 1, { name: "Alice Updated" });
// Delete a user
await typedStorage.users.delete().where((user) => user.id === 1);Type Safety & Error Handling in Action
Typed Storage catches type mismatches at compile time, preventing common data-related bugs before your code even runs.
// TypeScript will catch type mismatches during development
typedStorage.users.insert({
id: "not-a-number", // string instead of number
name: "Alice",
email: "alice@example.com",
});
// Error : Type 'string' is not assignable to type 'number'useLiveStorage
useLiveStorage is custom hook for react users that listen to the query and automatically rerneder the components hwn the data changed
const { data: users } = useLiveStorage(() =>
typedStorage.users.query().select({ id: true, name: true })
);
return (
<div>
{users.map((user) => (
<div key={user.id}>
<h2>{user.name}</h2>
<div>
<button
onClick={() => {
db.users.delete().where(equal({ id: user.id }));
// this will automatically rerender the comp without the deleted data
}}
>
Delete
</button>
</div>
</div>
))}
</div>
);