Skip to main content

TypeScript Local Storage

Introduction

When developing web applications, you often need to store data on the client side to persist information between page reloads or browser sessions. The browser's Local Storage API provides a simple key-value storage mechanism that allows you to save data directly in the user's browser. In this tutorial, we'll explore how to use Local Storage with TypeScript to create more type-safe and maintainable client-side storage solutions.

Local Storage offers several benefits:

  • Data persists even after the browser is closed and reopened
  • Storage capacity of approximately 5-10MB (varies by browser)
  • Simple API for storing and retrieving data
  • Synchronous operations that won't block the main thread for small operations

Understanding Local Storage Basics

Local Storage is part of the Web Storage API and provides a way to store string data with no expiration date. The data is stored as key-value pairs, and both keys and values must be strings.

Key Local Storage Methods

Here are the primary methods available in the Local Storage API:

typescript
// Store data
localStorage.setItem(key: string, value: string);

// Retrieve data
const value = localStorage.getItem(key: string);

// Remove specific data
localStorage.removeItem(key: string);

// Clear all data
localStorage.clear();

// Get number of stored items
const count = localStorage.length;

Using TypeScript with Local Storage

One challenge with Local Storage is that it only stores strings. When working with TypeScript, we want to leverage its type system while handling non-string data types. Let's explore several approaches to make Local Storage more TypeScript-friendly.

Basic Usage with TypeScript

Here's a simple example of storing and retrieving a string:

typescript
// Storing a string
localStorage.setItem('username', 'johndoe');

// Retrieving a string
const username: string | null = localStorage.getItem('username');

if (username) {
console.log(`Hello, ${username}!`); // Output: Hello, johndoe!
}

Working with Non-String Data Types

Since Local Storage only works with strings, we need to convert other data types to and from strings:

typescript
// Storing a number
const score: number = 100;
localStorage.setItem('userScore', score.toString());

// Retrieving a number
const storedScore: string | null = localStorage.getItem('userScore');
const userScore: number = storedScore ? parseInt(storedScore, 10) : 0;

console.log(typeof userScore, userScore); // Output: number 100

Storing and Retrieving Objects

For objects, we use JSON.stringify() and JSON.parse():

typescript
// Define a type for our user data
interface User {
id: number;
name: string;
isAdmin: boolean;
lastLogin: Date;
}

// Create a user object
const user: User = {
id: 123,
name: "Jane Smith",
isAdmin: false,
lastLogin: new Date()
};

// Store the object (converts to string)
localStorage.setItem('currentUser', JSON.stringify(user));

// Retrieve and parse the object
const storedUserString: string | null = localStorage.getItem('currentUser');
let storedUser: User | null = null;

if (storedUserString) {
const parsedUser = JSON.parse(storedUserString);

// Handle Date conversion (JSON.parse doesn't recognize Date objects)
parsedUser.lastLogin = new Date(parsedUser.lastLogin);

storedUser = parsedUser as User;
}

console.log(storedUser);
// Output: {id: 123, name: "Jane Smith", isAdmin: false, lastLogin: Date object}

Creating Type-Safe Wrappers for Local Storage

To make Local Storage more TypeScript-friendly, we can create wrapper functions:

typescript
/**
* Type-safe function to save any data to localStorage
*/
function saveToLocalStorage<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}

/**
* Type-safe function to retrieve data from localStorage
*/
function getFromLocalStorage<T>(key: string, defaultValue: T): T {
const data = localStorage.getItem(key);
if (data === null) {
return defaultValue;
}

return JSON.parse(data) as T;
}

/**
* Remove item from localStorage
*/
function removeFromLocalStorage(key: string): void {
localStorage.setItem(key, "");
}

Now let's use our type-safe functions:

typescript
// Define types
interface UserPreferences {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
}

// Save preferences
const preferences: UserPreferences = {
theme: 'dark',
fontSize: 16,
notifications: true
};

saveToLocalStorage<UserPreferences>('userPreferences', preferences);

// Retrieve preferences with default values
const storedPreferences = getFromLocalStorage<UserPreferences>(
'userPreferences',
{ theme: 'light', fontSize: 14, notifications: false }
);

console.log(storedPreferences.theme); // Output: dark
console.log(storedPreferences.fontSize); // Output: 16

Creating a Generic Local Storage Service Class

For more organized applications, we can create a service class to manage Local Storage operations:

typescript
class LocalStorageService {
/**
* Save data to localStorage
*/
public setItem<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}

/**
* Get data from localStorage
*/
public getItem<T>(key: string, defaultValue: T): T {
const data = localStorage.getItem(key);
if (data === null) {
return defaultValue;
}
return JSON.parse(data) as T;
}

/**
* Remove item from localStorage
*/
public removeItem(key: string): void {
localStorage.removeItem(key);
}

/**
* Clear all localStorage
*/
public clear(): void {
localStorage.clear();
}
}

// Create an instance
const storageService = new LocalStorageService();

// Usage
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}

// Add item to cart
const newItem: CartItem = {
id: 'prod-123',
name: 'Headphones',
price: 59.99,
quantity: 1
};

// Get current cart or initialize empty array
const currentCart = storageService.getItem<CartItem[]>('shoppingCart', []);

// Add new item and save
currentCart.push(newItem);
storageService.setItem('shoppingCart', currentCart);

// Retrieve cart
const savedCart = storageService.getItem<CartItem[]>('shoppingCart', []);
console.log(`Shopping cart has ${savedCart.length} items`); // Output: Shopping cart has 1 items

Handling Storage Events

When Local Storage is modified, a storage event is fired in other open tabs or windows from the same origin. This allows you to keep your application state in sync across multiple tabs:

typescript
window.addEventListener('storage', (event: StorageEvent) => {
if (event.key === 'shoppingCart') {
console.log('Shopping cart was updated in another tab');
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);

// Update your application state accordingly
if (event.newValue) {
const newCart = JSON.parse(event.newValue) as CartItem[];
// Update UI with new cart data
updateCartDisplay(newCart);
}
}
});

function updateCartDisplay(cart: CartItem[]): void {
// Implementation to update the UI
const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
console.log(`Cart updated: ${itemCount} items`);
}

Best Practices and Considerations

When working with Local Storage in TypeScript, keep these best practices in mind:

1. Handle Storage Limits

Local Storage has a size limit (typically 5-10MB). Implement error handling for storage limit errors:

typescript
function safelySaveToStorage<T>(key: string, value: T): boolean {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
if (error instanceof DOMException && (
error.code === 22 || // Chrome's quota exceeded error
error.name === 'QuotaExceededError' ||
error.name === 'NS_ERROR_DOM_QUOTA_REACHED')) {

console.error('Storage quota exceeded');
// Implement your fallback strategy here
return false;
}
throw error; // Re-throw if it's some other error
}
}

2. Add Version Control to Your Data Structures

As your application evolves, your stored data structure might change. Add versioning to make migrations possible:

typescript
interface VersionedData<T> {
version: number;
data: T;
}

function saveVersioned<T>(key: string, data: T, version: number): void {
const versionedData: VersionedData<T> = {
version,
data
};
localStorage.setItem(key, JSON.stringify(versionedData));
}

function getVersioned<T>(key: string, defaultData: T): { data: T, version: number } {
const stored = localStorage.getItem(key);
if (!stored) {
return { data: defaultData, version: 0 };
}

const parsed = JSON.parse(stored) as VersionedData<T>;
return { data: parsed.data, version: parsed.version };
}

// Example usage with data migration
const { data: userSettings, version } = getVersioned<UserSettings>('settings', defaultSettings);

if (version < 2) {
// Migrate from version 1 to version 2
// Add new properties, transform existing ones, etc.
userSettings.newProperty = 'default value';

// Save with new version number
saveVersioned('settings', userSettings, 2);
}

3. Sensitive Data Considerations

Remember that Local Storage is not secure for sensitive information:

typescript
// ❌ BAD: Don't store sensitive data in localStorage
localStorage.setItem('token', 'super-secret-jwt-token');

// ✅ GOOD: Use sessionStorage for temporary auth data
sessionStorage.setItem('temporaryAuth', 'session-token');

// ✅ BETTER: Use secure HTTP-only cookies for authentication
// (managed by your server, not accessible via JavaScript)

Real-World Application Example: User Theme Preferences

Let's create a complete example of a theme switcher that persists user preferences:

typescript
// Define our types
type Theme = 'light' | 'dark' | 'system';

interface ThemeSettings {
theme: Theme;
fontSize: number;
highContrast: boolean;
}

class ThemeManager {
private readonly STORAGE_KEY = 'app_theme_settings';
private currentSettings: ThemeSettings;

constructor() {
// Default settings
const defaultSettings: ThemeSettings = {
theme: 'system',
fontSize: 16,
highContrast: false
};

// Try to load saved settings or use defaults
const savedSettings = localStorage.getItem(this.STORAGE_KEY);

if (savedSettings) {
try {
this.currentSettings = JSON.parse(savedSettings) as ThemeSettings;
} catch (e) {
console.error('Failed to parse theme settings, using defaults');
this.currentSettings = defaultSettings;
}
} else {
this.currentSettings = defaultSettings;
}

// Apply the settings on initialization
this.applyTheme();
}

public getTheme(): Theme {
return this.currentSettings.theme;
}

public setTheme(theme: Theme): void {
this.currentSettings.theme = theme;
this.saveAndApply();
}

public getFontSize(): number {
return this.currentSettings.fontSize;
}

public setFontSize(size: number): void {
this.currentSettings.fontSize = size;
this.saveAndApply();
}

public getHighContrast(): boolean {
return this.currentSettings.highContrast;
}

public setHighContrast(enabled: boolean): void {
this.currentSettings.highContrast = enabled;
this.saveAndApply();
}

private saveAndApply(): void {
// Save to localStorage
localStorage.setItem(
this.STORAGE_KEY,
JSON.stringify(this.currentSettings)
);

// Apply the settings
this.applyTheme();
}

private applyTheme(): void {
const { theme, fontSize, highContrast } = this.currentSettings;

// Calculate actual theme based on system preference if needed
let effectiveTheme = theme;
if (theme === 'system') {
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}

// Apply theme to document
document.documentElement.setAttribute('data-theme', effectiveTheme);
document.documentElement.style.fontSize = `${fontSize}px`;

if (highContrast) {
document.documentElement.classList.add('high-contrast');
} else {
document.documentElement.classList.remove('high-contrast');
}

console.log(`Applied theme: ${effectiveTheme}, font size: ${fontSize}px, high contrast: ${highContrast}`);
}
}

// Usage example:
const themeManager = new ThemeManager();

// UI Controls example
document.getElementById('theme-light')?.addEventListener('click', () => {
themeManager.setTheme('light');
});

document.getElementById('theme-dark')?.addEventListener('click', () => {
themeManager.setTheme('dark');
});

document.getElementById('font-size-slider')?.addEventListener('change', (e) => {
const size = parseInt((e.target as HTMLInputElement).value, 10);
themeManager.setFontSize(size);
});

document.getElementById('high-contrast-toggle')?.addEventListener('change', (e) => {
const enabled = (e.target as HTMLInputElement).checked;
themeManager.setHighContrast(enabled);
});

Summary

In this tutorial, we've explored how to use TypeScript with the browser's Local Storage API to create type-safe and maintainable client-side storage solutions. We've covered:

  • The basics of Local Storage and its key methods
  • How to handle non-string data types using JSON serialization
  • Creating type-safe wrapper functions and service classes
  • Implementing storage event handling for multi-tab scenarios
  • Best practices including error handling, versioning, and security considerations
  • A real-world application example with a theme manager

By incorporating TypeScript's type system with Local Storage, we can create more robust web applications that preserve user settings and application state between sessions while catching potential errors at compile time.

Exercises

  1. Create a shopping cart service with TypeScript that uses Local Storage to persist items between page reloads.
  2. Implement a form that saves its state to Local Storage as the user types, so they can continue where they left off if they accidentally close the page.
  3. Build a "Recently Viewed Items" feature that stores the last 5 items a user has viewed on your website.
  4. Create a versioned storage system that can migrate user data from one schema version to another when your application updates.
  5. Implement a Local Storage quota monitor that warns users when they're approaching the storage limit and provides options to clear unnecessary data.

Additional Resources

Understanding how to properly use Local Storage with TypeScript is an essential skill for modern web development, allowing you to create more persistent and user-friendly applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)