
TypeScript: Kinderleichte und sichere Interaktion mit LocalStorage, SessionStorage, etc.
Von Jan Weiß am 13.03.2025
Eine gängige Aufgabe in JavaScript Applikationen ist es, in einen Store Werte mit einem Key zu speichern und diese dann auszulesen. Ob, man einen Cache in einer Server Applikation implementiert oder in einer Browseranwendung Werte in den LocalStorage schreibt. Man sieht: Es ist nicht entscheidend wo der JavaScript Code läuft und wohin die Daten “geschrieben” werden. Dieses abstrakte Konzept kann man überall finden. Deshalb möchte ich einen genaueren Blick darauf werfen und überlegen, wie man es idealerweise implementieren kann.
Abschauen bzw. von anderen lernen
Bei einem so gängigen Konzept ist es sicher, dass sich bereits viele andere Gedanken dazu gemacht haben. Schauen wir uns also zunächst an, was man in den Weiten der NPM Registry finden kann. Ein sehr spannendes Package ist “unstorage”. Es bietet Funktionen ein “Storage” Objekt zu erstellen, das ein verständliches Interface bietet für die essenziellen Interaktionen. Standardmäßig speichert der erzeugte Storage in-memory. Wohin der Storage allerdings schreibt, ist mit sogenannten “Driver” abstrahiert. Diese werden initial bei der Erstellung des Storage mitgegeben. Das Code Beispiel 1 zeigt, wie einfach die Verwendung von unstorage ist und implementiert einen simplen in-memory Cache.
// Code Beispiel 1
import { createStorage } from "unstorage";
import lruCacheDriver from "unstorage/drivers/lru-cache";
const storage = createStorage({
driver: lruCacheDriver({})
});
await storage.setItem('my-key', 'my value');
const myResultFromCache = await storage.getItem('my-key');
console.log(myResultFromCache); // logs: my value
Von unstorage kann man sich die homogene Storage Interaktionen mit drunterliegenden Driver abschauen.
Wie funktioniert hier die Typisierung? Bei unstorage sind die Storage Methoden wie z.B. getItem, oder setItem Generics. Mit getItem<string>(‘key’) kann man definieren, dass der zurückgegebene Wert ein String sein wird. Es geht jedoch bereits zu einem früheren Zeitpunkt! Schon bei der Erstellung des Storage können die Typen von Key und Value definiert werden. Code Beispiel 2, das von der offiziellen Dokumentation genommen ist, zeigt wie die Storage Einträge explizit definiert werden können. Wenn eine explizite Auflistung der Einträge nicht gewünscht ist, was wohl der gängigere Fall sein könnte, kann man “items” als Record definieren.
// Code Beispiel 2
type StorageDefinition = {
items: {
foo: string;
baz: number;
};
};
const storage = createStorage<StorageDefinition>();
await storage.has("foo"); // Ts will prompt you that there are two optional keys: "foo" or "baz"
await storage.getItem("baz"); // => string
await storage.setItem("foo", 12); // TS error: <number> is not compatible with <string>
await storage.setItem("foo", "val"); // Check ok
Ein weiteres spannendes Feature, das unstorage bietet sind sogenannte Namespaces. Mit ihnen lassen sich weitere Storage Objekte erzeugen, die automatisch einen prefix an einen Key eines Eintrags setzen. Dies kann nützlich sein, um zwischen Typen von Storage Einträgen zu unterscheiden.
Und nun zu etwas komplett Anderem. Wie wäre es, wenn man den Typen an einen Storage Key bindet? Diese Idee findet sich in Vue’s Dependency Injection. Hier gibt es den Generic Typ InjectionKey. Das Code Beispiel 3, aus der offiziellen Dokumentation, veranschaulicht wie er verwendet wird.
// Code Beispiel 3
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // providing non-string value will result in error
const foo = inject(key) // type of foo: string | undefined
Implementieren einer Abstraktion der SessionStorage Interaktionen
Wenn wir einen Storage möchten, der den SessionStorage eines Browsers verwendet, könnten wir nun den unstorage Driver “Browser” verwenden und ihn entsprechend konfigurieren. Aber aus Übungszweckend und weil wir lieber synchrone anstatt asynchroner Methoden hätten, machen wir es selbst. Mit den guten Ideen von unstorage und Vue im Gepäck, versuchen wir sehr simple helper Funktionen für den SessionStorage zu implementieren. Code Beispiel 4 legt die zwei Funktionen set und get an. Der SessionStorage erwartet, dass sowohl Key als auch Value Strings sind. Deshalb wird das zu speichernde Objekt mit JSON.stringify zu einem String umgewandelt und beim Lesen geparst.
// Code Beispiel 4
export const sessionStorage_set = <T>(
key: string,
payload: T
) => {
sessionStorage.setItem(key, JSON.stringify({ data: payload }));
};
export const sessionStorage_get = <T>(
key: string
): T | undefined => {
const item = sessionStorage.getItem(key);
if (!item) return;
try {
return JSON.parse(item).data as T;
} catch (_e) {
return undefined;
}
};
Jetzt wenden wir die Idee der Vue Dependancy Injection an und erlauben es, dass die Typinformation bereits im Key steckt (siehe Code Beispiel 5).
// CodeBeispiel 5
interface StorageConstraint<T> {}
export type StorageKey<T> = string & StorageConstraint<T>;
export const sessionStorage_set = <T>(
key: StorageKey<T>,
payload: T
) => {
sessionStorage.setItem(key, JSON.stringify(payload));
};
export const sessionStorage_get = <T>(
key: StorageKey<T>
): T | undefined => {
const item = sessionStorage.getItem(key);
if (!item) return;
try {
return JSON.parse(item) as T;
} catch (_e) {
return undefined;
}
};
Verwendung unserer Beispiel helper-Funktionen
Nun können wir in unserer JavaScript Applikation komfortabel mit dem SessionStorage interagieren und auch das Namespacing umsetzen. Im Code Beispiel 6 speichern und lesen wir ein ContactData Objekt. Alle Typen werden korrekt von TypeScript erzwungen. Super!
// Code Beispiel 6
interface ContactData {
phone: string;
mail: string;
}
const getContactDataStorageKey = (key: string): StorageKey<ContactData> =>
`MyData:${key}`;
const peterContactStorageKey = getContactDataStorageKey('peter');
sessionStorage_set(peterContactStorageKey, {
phone: '0123',
mail: 'inbox@gmail.com',
invalid: 'property' // Typescript complains which is good!
});
sessionStorage_get(peterContactStorageKey); // Return Type: ContactDate | undefined
The comments are closed.