Recipe · Objects and data

How do I type the result of JSON.parse in JavaScript?

  • Node.js (ES modules)
  • JSON.parse
  • unknown
  • @type
  • Checked with TypeScript 5.9

The problem JSON.parse gives me any, so a typo in a property name or a wrong type in the file goes unnoticed. How do I give the parsed value a type in JSDoc, and when is a cast not enough?

The short answer

Store the parsed value as unknown, check the parts you need, and return the typed object only after the checks pass:

settings.js settings.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
/**
 * @typedef {object} Settings
 * @property {"light" | "dark"} theme
 * @property {number} fontSize
 */

/**
 * @param {string} text
 * @returns {Settings}
 */
export function parseSettings(text) {
  /** @type {unknown} */
  const data = JSON.parse(text);
  if (
    typeof data === "object" &&
    data !== null &&
    "theme" in data &&
    (data.theme === "light" || data.theme === "dark") &&
    "fontSize" in data &&
    typeof data.fontSize === "number"
  ) {
    return { theme: data.theme, fontSize: data.fontSize };
  }
  throw new TypeError("settings.json does not hold settings");
}
export interface Settings {
  theme: "light" | "dark";
  fontSize: number;
}

export function parseSettings(text: string): Settings {
  const data: unknown = JSON.parse(text);
  if (
    typeof data === "object" &&
    data !== null &&
    "theme" in data &&
    (data.theme === "light" || data.theme === "dark") &&
    "fontSize" in data &&
    typeof data.fontSize === "number"
  ) {
    return { theme: data.theme, fontSize: data.fontSize };
  }
  throw new TypeError("settings.json does not hold settings");
}
Parse into unknown, check, then return the type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Inside the if, each check narrows data a little further: an object, not null, with a theme that is "light" or "dark" and a numeric fontSize. By the return, tsc knows enough to accept { theme: data.theme, fontSize: data.fontSize } as Settings.

Why it works

unknown is the type of a value you know nothing about: you can pass it around, but you cannot read its properties until you check. Each typeof, !== null, in and equality test narrows it, and since TypeScript 4.9 an in check also tells tsc the property exists. The function’s @returns {Settings} is then a fact about the data, not a hope.

When the JSON comes from a source you control and trust, a cast is shorter. It gives the value a type and checks nothing:

settings.js 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
/** @typedef {{ theme: "light" | "dark", fontSize: number }} Settings */

/**
 * @param {string} text
 * @returns {Settings}
 */
export function parseSettings(text) {
  /** @type {Settings} */
  const settings = JSON.parse(text);
  return settings;
}

const settings = parseSettings('{"theme":"dark","fontSize":"14px"}');
console.log(settings.fontSize.toFixed(0));
A @type cast on JSON.parse: quick, and only as good as the file.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

That example passes tsc, and it would throw at runtime: the file says "14px", so fontSize.toFixed is not a function. A cast moves the risk to the data.

The same thing in TypeScript

The TypeScript equivalent tab writes const data: unknown = JSON.parse(text); the checks are the same JavaScript. The cast version would be JSON.parse(text) as Settings.

What goes wrong

Reading a property of an unknown value before checking it is TS18046, which is the point: tsc makes you look before you use it.

settings.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: settings.js(6,10): error TS18046: 'data' is of type 'unknown'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. settings.js(6,10): error TS18046: 'data' is of type 'unknown'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. settings.js(6,10): error TS18046: 'data' is of type 'unknown'. checked 21 October 2026
/** @param {string} text */
export function readTheme(text) {
  /** @type {unknown} */
  const data = JSON.parse(text);
  return data.theme;error TS18046: 'data' is of type 'unknown'.
}
Reading a property of unknown data.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • any spreads. Leave JSON.parse(text) untyped and everything read from it is any too, through every function it reaches; a cast or an unknown stops that at the source.
  • A @type on a variable is a cast for any values. /** @type {Settings} */ const settings = JSON.parse(text) is accepted because any fits every type; the same tag on a real value of the wrong type is an error.
  • Dates and maps do not survive JSON. A Date comes back as a string. Type the parsed shape as it is in the file, then convert.

Questions

Why does JSON.parse return any and not unknown?

That is how TypeScript's library declares it, in TypeScript 5.9 as before. Annotating the result as unknown yourself gets the safer behavior.

Is there a way to check the data without writing the checks by hand?

A schema library that checks data at runtime and also exposes the matching type does both jobs from one definition. The guide on runtime types and annotations shows the hand-written version of the same idea.

Sources

Last reviewed 21 October 2026.