A type-safe serialisation and validation wrapper for string storage APIs like localStorage and sessionStorage, using Standard Schema for validation.
Installation
npm install safeway
Usage
Synchronous Storage
To create a type safe store, provide a key and a schema. The key will be where the value is stored, and the schema will be used to validate the value when getting it from storage. (When setting a value, no runtime validation happens, only TypeScript type checking.)
Defaults to localStorage and JSON serialisation.
import { createStore } from "safeway"; import { z } from "zod"; const store = createStore("count", z.number()); store.set(1); // typed based on schema input (number) console.log(store.get()); // 1 - typed based on schema output (number | undefined) store.remove(); console.log(store.get()); // undefined (still typed as number | undefined)
Schemas are allowed to include transformations, in which case store.set's parameter will be based on the schema's expected input.
import { createStore } from "safeway"; import { z } from "zod"; const store = createStore( "count", z.number().transform((count) => ({ count })), ); store.set(1); // typed based on schema input (number) console.log(store.get()); // { count: 1 } - typed based on schema output ({ count: number } | undefined)
Multi-store
If you want to create a store with multiple keys and types, you can use createMultiStore instead.
import { createMultiStore } from "safeway"; import { z } from "zod"; const store = createMultiStore({ count: z.number(), name: z.string(), }); store.set("count", 1); // typed based on schema input (number) console.log(store.get("count")); // 1 - typed based on schema output (number | undefined)
Custom serialisation
If JSON doesn't cover all your needs, you can provide your own serialisation methods (or use a compatible library like superjson). Should include parse and stringify methods.
import { createStore } from "safeway"; import { z } from "zod"; import superjson from "superjson"; const store = createStore("counts", z.set(z.number()), { serializer: superjson, });
Custom storage
If you want to use a different storage instance, you can provide it. Should include getItem, setItem and removeItem methods.
import { createStore } from "safeway"; import { z } from "zod"; const store = createStore("count", z.number(), { storage: sessionStorage, });
Building a custom store creator
Instead of providing the same config every time, you can build a custom store creator with your own defaults.
import { buildStoreCreator } from "safeway"; import { z } from "zod"; import superjson from "superjson"; const createSuperStore = buildStoreCreator({ serializer: superjson, storage: sessionStorage, }); const store = createSuperStore("count", z.number());
buildStoreCreator also has a multi-store equivalent, buildMultiStoreCreator.
Asynchronous Storage
createStore requires both storage and schemas to be synchronous. If you need asynchronous storage and/or schemas, use createAsyncStore instead. (createMultiStore also has an asynchronous equivalent, createAsyncMultiStore)
The API is the same as createStore, but all methods return promises.
Defaults to localStorage and JSON serialisation.
import { createAsyncStore } from "safeway"; import { z } from "zod"; import AsyncStorage from "@react-native-async-storage/async-storage"; const store = createAsyncStore("count", z.number(), { storage: AsyncStorage, }); await store.set(1); // typed based on schema input (number) console.log(await store.get()); // 1 - typed based on schema output (number | undefined) await store.remove(); console.log(await store.get()); // undefined (still typed as number | undefined)
Supports all the same customisation options as the synchronous storage, but with asynchronous storage methods allowed. A store creator can be built with buildAsyncStoreCreator. (buildAsyncMultiStoreCreator also exists)