
Why Type Safety for Local Storage Is Essential
Browser local storage is powerful but risky. The simple key-value API makes it easy to store strings, but complex data requires manual serialization and deserialization. Without type checking, you risk runtime errors when data doesn’t match your expectations.
Common local storage problems include:
- Storing complex objects without proper serialization
- Retrieving data with unexpected structure
- Missing properties in stored objects
- No schema validation for stored data
A typesafe approach solves these issues by enforcing data structure at compile time.
Building a Schema-Based Foundation for Local Storage
First, define a schema that describes all possible data structures you’ll store:
import { z } from 'astro/zod';
// Define schemas for different types of dataexport const ZContactFormSchema = z.object({ name: z.string(), email: z.string().email(), message: z.string(), submitted: z.boolean().optional(),});
export const ZAnalyticsSchema = z.object({ pageViews: z.record(z.string(), z.number()), lastVisit: z.string(), // ISO date string});
// Create the main schema object with all possible storage keysexport const ZLocalDataSchemas = z.object({ ['contact-form']: ZContactFormSchema, ['analytics']: ZAnalyticsSchema, ['color-picker-history']: z.array(z.string()), // Add more schemas as needed});
// Export the inferred TypeScript typeexport type LocalDataSchemas = z.infer<typeof ZLocalDataSchemas>;
This schema creates a contract for your data. Now TypeScript knows exactly what structure to expect for each storage key.
Limited Time Launch Sale
Want to skip the setup? Our Astro starter template includes a ready-to-use typesafe local storage system with pre-built schemas!
GET 60% OFF!Implementing a Type-Safe Storage Utility
Next, create a utility that enforces these types when reading from or writing to local storage:
import { CONFIG } from '../../constants';import type { LocalDataSchemas } from '../../schemas/local-data';
// Prefix keys to avoid collisions with other appsconst dataKey = (key: string) => `${CONFIG.APP_NAME}:${key}`;
// JSON-safe type definitionstype JsonPrimitive = string | number | boolean | null;type JsonArray = JsonValue[];type JsonObject = { [key: string]: JsonValue };type JsonValue = JsonPrimitive | JsonObject | JsonArray;
// Convert non-JSON-safe types to stringtype JsonSafe<T> = { [P in keyof T]: T[P] extends JsonValue ? T[P] : T[P] extends Date ? string : T[P] extends object ? { [K in keyof T[P]]: JsonSafe<T[P][K]> } : string;};
// Get data with type safetyexport const getData = <T extends keyof LocalDataSchemas, R = LocalDataSchemas[T]>(location: T): Partial<JsonSafe<R>> | undefined => { let localData: Partial<JsonSafe<R>> | undefined; try { const temp = localStorage.getItem(dataKey(location)); if (temp) localData = JSON.parse(temp) as JsonSafe<R>; } catch (e) { console.error(`Error loading data for ${location}:`, e); } return localData;};
// Set data with type safetyexport const setData = <T extends keyof LocalDataSchemas, R = LocalDataSchemas[T]>(location: T, data: Partial<JsonSafe<R> | R>): void => { try { localStorage.setItem(dataKey(location), JSON.stringify(data)); } catch (e) { console.error(`Error saving data for ${location}:`, e); }};
// Delete dataexport const deleteData = <T extends keyof LocalDataSchemas>(location: T): void => { try { localStorage.removeItem(dataKey(location)); } catch (e) { console.error(`Error deleting data for ${location}:`, e); }};
How to Use Type-Safe Local Storage in Your Application
// Example usageimport { getData, setData, deleteData } from '../lib/client/storage';
// TypeScript knows exactly what shape this data should haveconst contactForm = getData('contact-form');
// TypeScript will error if you try to access a property that doesn't existconst email = contactForm?.email; // ✓ Validconst phone = contactForm?.phone; // ✗ Error: Property 'phone' does not exist
// TypeScript will error if you try to save data with the wrong shapesetData('contact-form', { name: 'John Doe', message: 'Hello world', // TypeScript would error if we tried to add an unexpected field});
// Delete data when no longer neededdeleteData('contact-form');
Creating a Form Data Helper for Automatic Storage
For forms, we can create a helper function that automatically saves form data to local storage:
// Generic form data handlerexport const genericSaveDataHandler = <T extends keyof LocalDataSchemas>( key: T, options?: { dontSave?: (keyof LocalDataSchemas[T])[]; }) => { return (formData: FormData) => { const data = getData<T>(key) || {};
if (options?.dontSave) { options.dontSave.forEach((key) => formData.delete(String(key))); }
const entries = Object.fromEntries(formData.entries());
const newData = { ...data, ...entries, } as unknown as JsonSafe<LocalDataSchemas[T]>;
setData(key, newData); };};
Advanced Features for Enhanced Local Storage Management
- Expiration dates: Add TTL (time-to-live) for stored data
- Compression: Compress large data before storing
- Migration: Handle schema changes with version tracking
By implementing typesafe local storage, you’ll catch errors at compile time rather than runtime, making your applications more reliable and easier to maintain.
Related Articles

How To Create A Secure Staging Environment For Cloudflare Pages
Testing website changes before they go live is essential for maintaining a professional online presence. Learn how to create a secure, password-protected staging environment for your Cloudflare Pages site.

How To Redirect .pages.dev to Your Domain
Learn how to redirect .pages.dev to your domain with this step-by-step guide

Schedule Cloudflare Pages Builds
Schedule your Cloudflare Pages builds to run at specific times. No external services needed.

Fix Bad Indentation Of A Mapping Entry Error In Astro
Troubleshoot and resolve the common "bad indentation of a mapping entry" error in AstroJS with this practical guide.