Having fun deconstructing the ‚Äčlocalstorage in TypeScript ūü§ô

Photo by Katya Ross on Unsplash
I recently implemented some features with the localstorage. While I always had read values using the getItem() method of the interface, I replaced this approach in my recent work with deconstruction of the storage object.
For no particular reason. I just like to deconstruct things, a lot ūüėĄ.

‚ÄčOld school

‚ÄčBack in the days - until last few weeks ūüėČ - I would have probably implemented a function to read a stringified ‚Äčobject‚Äč from the storage as following:
type MyType = unknown; const isValid = (value: string | null): value is string => [null, undefined, ""].includes(value) const oldSchool = (): MyType | undefined => { const value: string | null = localStorage.getItem("my_key"); if (!isValid(value)) { return undefined; } return JSON.parse(value); };
‚Äči.e. I would have first get the ‚Äčstring‚Äč value (stringified ‚ÄčJSON.stringify() representation of the object I would have saved in the storage) using ‚ÄčgetItem()‚Äč before double checking its validity and parsing it back to an object.

‚ÄčNew school

‚ÄčWhile I nowadays keep following previous logic ("read, check validity and parse"), I am now deconstructing the storage to read the value.
const newSchool = (): MyType | undefined => { const { my_key: value }: Storage = localStorage; if (!isValid(value)) { return undefined; } return JSON.parse(value); };
Again, no particular reason but, isn't it shinier? ūüĎ®‚Äćūüé®
This approach is possible in TypeScript because the ‚ÄčStorage‚Äč interface - representing the Storage API - is actually declared as a map of keys of ‚Äčany‚Äč types.
interface Storage { readonly length: number; clear(): void; getItem(key: string): string | null; key(index: number): string | null; removeItem(key: string): void; setItem(key: string, value: string): void; // HERE ūüėÉ [name: string]: any; [name: string]: any; }

‚ÄčSSR & pre-rendering

The ‚Äčlocalstorage is a readonly property of the ‚Äčwindow‚Äč interface - i.e. it exists only in the browser. To prevent my SvelteKit's static build to crash when I use it, I set an ‚Äčundefined‚Äč fallback value for the NodeJS context.
‚ÄčMoreover, as in addition to the deconstruction pattern, I also like to inline everything (ūüėĄ). So, I came up with the following code snippet to solve my inspiration:
import { browser } from "$app/env"; const newSchool = (): MyType | undefined => { const { my_key: value }: Storage = browser ? localStorage : ({ my_key: undefined } as unknown as Storage); if (!isValid(value)) { return undefined; } return JSON.parse(value); };

‚ÄčGeneric

‚ÄčAt this point you might say "Yes David, good, this is cool and stuffs but, what about reusability?". To which, I would answer "Hold my beer, you can dynamically deconstruct objects" ūüėČ.
const newSchool = <T>(key: string): T | undefined => { const { [key]: value }: Storage = browser ? localStorage : ({ [key]: undefined } as unknown as Storage); if (!isValid(value)) { return undefined; } return JSON.parse(value); };

‚ÄčSummary

Returning ‚Äčundefined‚Äč is convenient for demo purpose but, in actual implementations - such as the one I just unleashed this morning in Papyrs (a web3 blogging platform) - it might be useful to rather use default fallback values.
Therefore, here is the final form of my generic function to read items that have been saved in the ‚Äčlocalstorage‚Äč in TypeScript using fun stuffs such as deconstructing objects, assertion and generic.
import { browser } from "$app/env"; const isValid = (value: string | null): value is string => [null, undefined, ""].includes(value); const getStorageItem = <T>({ key, defaultValue, }: { key: string; defaultValue: T; }): T => { const { [key]: value }: Storage = browser ? localStorage : ({ [key]: undefined } as unknown as Storage); if (!isValid(value)) { return defaultValue; } return JSON.parse(value); };
‚ÄčTo infinity and beyond
David

‚Äč‚ÄčFor more adventures, follow me on Twitter ūüĖĖ