A type-safe wrapper around the browser's LocalStorage
and SessionStorage
API's. The motivation behind this package is two fold:
- When working with large teams, ensuring that key-names are consistent and follow certain conventions can be combersome
- Ensuring that only values of a certain type are stored for each key can require can be difficult because the API's require string-values when storing data
The TypedStorage
API allows you define a schema for your data and restricts storing data to only that which matches your key's corresponding value type.
npm i @figliolia/typed-storage
# or
yarn add @figliolia/typed-storage
import { TypedStorage } from "@figliolia/typed-storage";
export interface Schema {
JWT: string;
userId: number;
shoppingCart: { item: string, price: number }[];
}
const LocalStorage = new TypedStorage<Schema>(localStorage);
// or
const SessionStorage = new TypedStorage<Schema>(sessionStorage);
When using your TypedStorage
instances, your data-type is preserved when setting and getting:
import { LocalStorage } from "./path/to/myLocalStorage";
LocalStorage.setItem("shoppingCart", [
{item: "Bananas", price: 3.00 },
{item: "Apples", price: 2.50 },
// Stringified under the hood
]);
const cart = LocalStorage.getItem("shoppingCart"); // parsed under the hood
// { item: string, price: number }[] | null
const cart = LocalStorage.getItem("cart"); // mispelled key
// Fails typescript validation!
The TypedStorage
API supports storing any data-type that is JSON-valid. For keys with values of type object
or array
, JSON.parse()
will be used to deserialize your data upon retreival.
For keys with values of type string
or number
, best effort parsing is used to determine the correct type on retrieval. For example if the value being retrieved can be safely converted to an integer, float, or BigInt
, it will be. Otherwise the value will be returned as a string.
For instances where your key-value pair requires a customized serialization or deserialization technique, you can instantiate your TypedStorage
along with your deserializer mapped to your key:
import { TypedStorage } from "@figliolia/typed-storage";
interface Schema {
meyKey: Map<string, number>; // (invalid JSON)
}
const MyStorage = new TypedStorage<Schema>(localStorage, {
meyKey: {
// Convert map to object and stringify it for storage
serialize: (map) => {
const obj: Record<string, number> = {};
for(const [key, value] of map) {
obj[key] = value;
}
return JSON.stringify(map);
},
// Convert stored object back into a Map for runtime usage
deserialize: (map) => {
const obj = JSON.parse(map);
const result = new Map();
for(const key in obj) {
result.set(key, obj[key]);
}
return result;
}
}
});
Serializers can also be used in instances where you wish to handle numeric values as strings
import { TypedStorage } from "@figliolia/typed-storage";
interface Schema {
floatValue: string; // Handle float as strings
}
const MyStorage = new TypedStorage<Schema>(localStorage, {
floatValue: {
// Prevent float-like string from being returned as a number
deserialize: (floatValue) => floatValue;
}
})
Because TypedStorage
parses using a best-effort interpretation of the value, if your string contains only numeric characters, it'll be parsed as a number regardless of your schema definition.
If you run into a case such as this and you wish to preserve string-types for numeric values, provide a serialize
method for that key that simply returns the value as is.
This library also provides an enhancement to the TypedStorage
API called LiveStorage
. It works identically to TypedStorage
with the exception that LiveStorage
allows you to synchronize your application logic with the data you store:
import { LiveStorage } from "@figliolia/typed-storage";
export interface Schema {
JWT: string;
userId: number;
shoppingCart: { item: string, price: number }[];
}
const LocalStorage = new TypedStorage<Schema>(localStorage);
// or
const SessionStorage = new TypedStorage<Schema>(sessionStorage);
// Let's update our UI whenever our `shoppingCart` changes
const currency = new Intl.NumberFormat('en-us', {
style: 'currency',
currency: 'USD'
});
const checkoutUI = document.getElementById("checkout");
LocalStorage.on("shoppingCart", cart => {
if(cart === null) {
// the key was deleted
checkoutUI.textContent = currency.format(0.00);
return;
}
// Update your checkout UI's total cost $
const newPrice = cart.reduce((acc, next) => {
acc += next.price;
return acc;
}, 0);
checkoutUI.textContent = currency.format(newPrice);
});
In the example above, we'll update our checkout UI
whenever the user's shopping cart is updated.
Use LiveStorage
in areas where your business logic depends heavily on reads/writes to storage. If your application is performing read/writes in logic that spans multiple features or modules, you may find the that LiveStorage
API allows you to write more centralized logic in otherwise complex scenarios.