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:
// 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:
// 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:
// 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()
:
// 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:
/**
* 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:
// 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:
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:
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:
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:
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:
// ❌ 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:
// 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
- Create a shopping cart service with TypeScript that uses Local Storage to persist items between page reloads.
- 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.
- Build a "Recently Viewed Items" feature that stores the last 5 items a user has viewed on your website.
- Create a versioned storage system that can migrate user data from one schema version to another when your application updates.
- 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! :)