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 🖖