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