Recipe · Node and platforms

How do I type values from localStorage in JavaScript?

  • Browser (no bundler)
  • localStorage
  • sessionStorage
  • @template
  • Checked with TypeScript 5.9

The problem I keep the cart in localStorage as JSON. tsc complains that getItem might return null, and everything I read back is any. How do I type stored values in a JSDoc-checked project?

The short answer

Read and write through two small helpers: readJson handles the missing key and bad JSON, and its @template lets each caller say what it expects:

storage.js storage.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @template T
 * @param {string} key
 * @param {T} fallback
 * @returns {T}
 */
export function readJson(key, fallback) {
  const text = localStorage.getItem(key);
  if (text === null) return fallback;
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

/**
 * @param {string} key
 * @param {unknown} value
 */
export function writeJson(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

/** @typedef {{ sku: string, quantity: number }} CartLine */

/** @type {CartLine[]} */
const cart = readJson("cart", []);
cart.push({ sku: "LAMP-01", quantity: 1 });
writeJson("cart", cart);
sessionStorage.setItem("lastVisit", new Date().toISOString());
export function readJson<T>(key: string, fallback: T): T {
  const text = localStorage.getItem(key);
  if (text === null) return fallback;
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

export function writeJson(key: string, value: unknown): void {
  localStorage.setItem(key, JSON.stringify(value));
}

type CartLine = { sku: string; quantity: number };

const cart: CartLine[] = readJson("cart", []);
cart.push({ sku: "LAMP-01", quantity: 1 });
writeJson("cart", cart);
sessionStorage.setItem("lastVisit", new Date().toISOString());
A small typed wrapper around localStorage.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

cart is a CartLine[], so cart.push checks each line, and nothing else in the program touches getItem or JSON.parse directly.

Why it works

localStorage.getItem returns string | null: null when the key was never written or was removed. readJson handles that case and the JSON.parse error, and returns the fallback instead. The type parameter T is inferred from the fallback (or from the variable’s @type, as for cart), so the helper serves every key.

The same thing in TypeScript

The TypeScript equivalent tab writes the helper as readJson<T>(key: string, fallback: T): T. JavaScript cannot pass <CartLine[]> at the call, so the @type on cart does that job.

What goes wrong

Passing getItem’s result straight to JSON.parse fails because it may be null, and setItem takes only strings (TS2345 for both):

storage.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: storage.js(3,25): error TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'. storage.js(6,31): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. storage.js(3,25): error TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'. storage.js(6,31): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. storage.js(3,25): error TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'. storage.js(6,31): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. checked 21 October 2026
const saved = localStorage.getItem("cart");
const cart = JSON.parse(saved);error TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'.
  Type 'null' is not assignable to type 'string'.

localStorage.setItem("count", 3);error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
console.log(cart);
getItem may return null, and setItem takes only strings.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • Storage can throw. Browsers may refuse storage (private windows, full quota, blocked site data); wrap setItem in try when losing the value matters.
  • Numbers come back as strings. localStorage.setItem("count", String(3)) stores "3"; read it with Number(…) or through readJson.
  • Dates do not survive JSON. A Date is stored as its ISO string; type the stored shape with string and convert when you read it.

Questions

Is the type of the stored value checked when it is read back?

No. readJson returns whatever JSON.parse produces, typed as T on trust. Data saved by an older version of your code, or edited by hand, can be different; check it with a type guard when that matters.

Does the same code work for sessionStorage?

Yes. Both are Storage objects with the same methods; add a parameter of type Storage to the helpers to use either one.

Sources

Last reviewed 21 October 2026.