Using localStorage with TypeScript: A Developer’s Guide

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

Utilizing browser storage is a key aspect of creating persistent, user-friendly web applications. This guide will walk you through integrating localStorage with TypeScript, unlocking robust, type-safe storage solutions.

Understanding localStorage

Before diving into TypeScript specifics, let’s first unpack what localStorage is. localStorage is a part of the Web Storage API, allowing you to store key-value pairs in a web browser with no expiration date. This data persists through page refreshes and browser restarts.

Example:

localStorage.setItem('key', 'value');
console.log(localStorage.getItem('key')); // Output: value

Generic localStorage Functions in TypeScript

When using TypeScript, you benefit from having type information. Let’s start by creating simple, typed wrappers for localStorage.

function setItem<T>(key: string, value: T): void {
  localStorage.setItem(key, JSON.stringify(value));
}

function getItem<T>(key: string): T | null {
  const item = localStorage.getItem(key);
  return item ? JSON.parse(item) as T : null;
}

This basic example serializes and deserializes the data to and from JSON, adding a thin layer of type safety.

TypeScript Interfaces for Structured Data

Storing structured data greatly benefits from utilizing TypeScript interfaces to ensure consistent access.

interface UserPreferences {
  darkMode: boolean;
  fontSize: number;
}

// Save preferences
setItem<UserPreferences>('preferences', { darkMode: true, fontSize: 14 });
// Retrieve preferences
const preferences = getItem<UserPreferences>('preferences');

Error Handling with Try/Catch

Dealing with storage might entail exceptions, for example when the storage quota is exceeded. Wrapping storage operations with try/catch blocks can aide in handling these scenarios gracefully.

try {
  setItem('key', potentiallyLargeData);
} catch (e) {
  console.error('Could not save data:', e);
}

TypeGuard for localStorage Data

Adding an extra layer of safety, a TypeGuard confirms that the retrieved data is of the expected type.

function isUserPreferences(object: any): object is UserPreferences {
  return 'darkMode' in object && 'fontSize' in object;
}

const rawData = getItem<unknown>('preferences');
if (isUserPreferences(rawData)) {
  // rawData is now known to be UserPreferences
}

Advanced Techniques

For more complex scenarios, you can employ techniques like versioning the storage schema, event-driven updates, and abstract classes for more complex storage patterns.

Schema Versioning

In case your data structure changes, version your schemas to avoid mismatches.

interface VersionedUserPreferences {
  version: number;
  preferences: UserPreferences;
}

Event-driven updates: Listen to storage events to synchronize data.

window.addEventListener('storage', (event) => {
  if (event.key === 'preferences') {
    // Handle the updated preferences
  }
});

Abstract Storage Class: For complex scenarios, an abstract class can encapsulate storage logic.

abstract class StorageService<T> {
  protected abstract getKey(): string;
  protected abstract isCorrectType(object: any): object is T;

  public save(value: T): void {
    if (this.isCorrectType(value)) {
      setItem(this.getKey(), value);
    }
  }

  public load(): T | null {
    const rawData = getItem<unknown>(this.getKey());
    if (rawData && this.isCorrectType(rawData)) {
      return rawData;
    }
    return null;
  }
}

This approach offers reusability and encapsulation across different parts of your application.

Conclusion

By combining localStorage with TypeScript’s typing system, you create a resilient and reliable browser storage solution for your web applications. The use of interfaces, type guards, and error handling can significantly enhance the functionality and maintainability of your storage-related code. Lean on these techniques to bring your data handling to new levels of sophistication.