Skip to content

Instantly share code, notes, and snippets.

@markusand
Created August 26, 2025 07:36
Show Gist options
  • Select an option

  • Save markusand/39281e9272ede66b72ca7fb1f657adc6 to your computer and use it in GitHub Desktop.

Select an option

Save markusand/39281e9272ede66b72ca7fb1f657adc6 to your computer and use it in GitHub Desktop.
Type-safe wrapper for localStorage supporting primitives, Date, arrays, and nested objects. Includes set, get, remove, clear and a Vue 3 ref that auto-syncs with storage.
export type Storable =
| string
| number
| boolean
| null
| Date
| Storable[]
| undefined
| { [key: string]: Storable };
type StoredData = {
__type: string;
value: unknown;
};
const getType = (value: Storable): string => {
if (value === null) return 'null';
if (value instanceof Date) return 'Date';
if (Array.isArray(value)) return 'Array';
if (typeof value === 'object') return 'Object';
return typeof value;
};
const encode = (data: Storable): StoredData => {
const serializers: Record<string, (value: Storable) => unknown> = {
Date: value => (value as Date).toISOString(),
Array: value => (value as Storable[]).map(encode),
Object: value => Object.fromEntries(
Object.entries(value as object).map(([k, v]) => [k, encode(v)]),
),
primitive: value => value,
};
const primitives = ['number', 'string', 'boolean', 'null'];
const type = getType(data);
const key = primitives.includes(type) ? 'primitive' : type;
return { __type: type, value: serializers[key]?.(data) ?? data };
};
const decode = (data: StoredData): Storable => {
const parsers: Record<string, (value: unknown) => Storable> = {
Date: value => new Date(value as string),
Array: value => (value as StoredData[]).map(decode),
Object: value => Object.fromEntries(
Object.entries(value as object).map(([k, v]) => [k, decode(v)]),
),
primitive: value => value as Storable,
};
const primitives = ['number', 'string', 'boolean', 'null'];
const key = primitives.includes(data.__type) ? 'primitive' : data.__type;
return parsers[key]?.(data.value);
};
export default {
set: <T extends Storable>(key: string, value: T): void => {
const encoded = encode(value);
localStorage.setItem(key, JSON.stringify(encoded));
},
get: <T extends Storable>(key: string): T | null => {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as StoredData;
return decode(parsed) as T;
} catch {
return null;
}
},
remove: localStorage.removeItem,
clear: localStorage.clear,
};
import { ref, watch } from 'vue';
import storage, { type Storable } from '/@/services/typed-storage';
export default <T extends Storable>(key: string, initial: T) => {
const data = ref<T>(storage.get(key) ?? initial);
watch(data, value => storage.set(key, value), { deep: true });
return data;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment