The browser's localStorage keeps key-value pairs around forever — there is no built-in way to say "remember this auth token, but only for an hour." You're going to build a thin wrapper that adds a time-to-live (TTL): a value you save with set(key, value, ttlMs) reads back normally until ttlMs milliseconds have passed, and null after that. This is the same caching-with-expiry idea behind HTTP Cache-Control: max-age, Redis EXPIRE, and session cookies.
The catch: a real localStorage only stores strings, and it has no concept of time. So the wrapper has to write the expiry deadline into storage alongside the value, then compare against the clock on every read.
// backingStore: a localStorage-shaped store — { getItem, setItem, removeItem }.
// Defaults to an in-memory adapter so this runs with no real localStorage.
// now: () => number — current time in ms. Defaults to () => Date.now().
function createStorageWithExpiry(
backingStore?,
now?,
): {
set(key: string, value: unknown, ttlMs: number): void; // persist value + deadline
get(key: string): unknown | null; // value if live, else null
remove(key: string): void; // delete a key outright
};
const store = createStorageWithExpiry();
store.set('token', 'abc123', 60_000); // good for 60 seconds
store.get('token'); // → 'abc123' (read immediately)
// ...61 seconds later...
store.get('token'); // → null (the TTL has elapsed)
// Values are not limited to strings — objects round-trip too.
store.set('user', { id: 7, name: 'Ada' }, 5_000);
store.get('user'); // → { id: 7, name: 'Ada' }
// Overwriting restarts the clock: this is good for a fresh 5_000ms.
store.set('user', { id: 7, name: 'Ada' }, 5_000);
localStorage and you must not depend on the wall clock for deterministic tests. createStorageWithExpiry(backingStore, now) lets a test pass a Map-backed stub and a clock it advances by hand. In a real browser you'd call createStorageWithExpiry(window.localStorage). See the solution's Notes for why this matters.expiresAt = now() + ttlMs, not the raw ttlMs. Storage has no clock of its own, so a read has to compare a stored absolute timestamp against the current time.get finds an expired entry, remove it from the backing store before returning null — don't let dead rows pile up.ttlMs is a positive number of milliseconds. Don't worry about negative or zero TTLs, NaN, or cross-tab synchronization — those are out of scope here.JSON.stringify / JSON.parse. Functions, undefined, and circular references are out of scope.