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> );