JSON & Result Types

The Course
•
5 min read

JSON.parse checks JSON syntax, not your application’s shape. Its standard TypeScript return type is any for historical compatibility. Assign the result immediately to unknown so unchecked assumptions do not spread: const value: unknown = JSON.parse(text).

Two independent failures are possible. Malformed text such as ”{” cannot be parsed. Valid JSON such as {"minutes":"many"} can be parsed but still be unsuitable for your application. Handle both before returning a trusted Session.

Use try/catch for the syntax operation. Under strict checking, a caught error is unknown. If you want its message, first test error instanceof Error; thrown values are not guaranteed to be Error objects. A short user-facing parser can also return its own stable message rather than exposing implementation details.

Return a discriminated Result instead of letting expected invalid input crash the program. The success variant holds trusted data; the error variant holds a useful message. Keep the try block narrow so a later programming bug is not silently relabeled as invalid JSON.

A successful parse followed by validation establishes a boundary. Functions after that boundary can accept Session and focus on their job. Repeating the full validator inside every arithmetic helper would obscure that separation.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
function readText(json: string): string {
  let value: unknown;

  try {
    value = JSON.parse(json);
  } catch {
    return "Invalid JSON";
  }

  if (typeof value !== "string") {
    return "Expected text";
  }

  return value.trim();
}

console.log(readText('" Types "'));
console.log(readText("25"));
console.log(readText("{"));

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
Expected text
Invalid JSON

Read a real compiler error

The program can catch a value of any runtime type. The checker does not assume it is an Error instance.

Find and fix the type errorTypeScript
try {
  JSON.parse("{");
} catch (error) {
  console.log(error.message);
}

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Use error instanceof Error before reading message, or show your own stable parser message. An assertion would not prove what was thrown.

Separate syntax failure from domain failure

JSON.parse can succeed with data of the wrong type. Catching a syntax error is therefore not sufficient validation. Store the parsed result as unknown, then narrow its shape and fields before returning a trusted value.

A Result union can distinguish expected failure from a valid zero or empty collection. Keep the discriminant explicit. Returning null or a fallback for every problem may hide whether the text was malformed or simply described the wrong domain object.

Separate syntax failure from domain failure: traceTypeScript
function read(text: string): string {
  let value: unknown;

  try {
    value = JSON.parse(text);
  } catch {
    return "Syntax error";
  }

  return typeof value === "number" ? `Number ${value}` : "Wrong shape";
}

console.log(read("0"));
console.log(read('"0"'));
console.log(read("{"));

Enable JavaScript to edit and type-check this example.

Check your prediction
Number 0
Wrong shape
Syntax error

The string zero is valid JSON but not the numeric shape this function expects. Each output identifies a distinct boundary instead of treating every failure as a parse exception.

Separate syntax failure from domain failure: contrastTypeScript
type Result =
  | {
      ok: true;

      value: number;
    }
  | {
      ok: false;

      message: string;
    };

function label(result: Result): string {
  return result.ok ? String(result.value) : result.message;
}

console.log(label({ ok: true, value: 0 }));
console.log(label({ ok: false, message: "Invalid count" }));

Enable JavaScript to edit and type-check this example.

Check your prediction
0
Invalid count

The union makes zero a success independently of its truthiness. A later decoder can add integer and range rules before constructing that success branch.

Try the rule on new inputs

Classify malformed text, valid null, valid wrong-type text and valid data. State which stage rejects each case and preserve the original application state until both parsing and decoding succeed.

Your turn

Exercise

JSON & Result Types: practise

Write parseCount(json) returning { kind:“ok”; count:number } or { kind:“error”; message:string }. Invalid syntax uses “Invalid JSON”. Valid JSON that is not a finite nonnegative integer uses “Invalid count”. Display 0, Invalid count and Invalid JSON for “0”, ‘“5”’ and ”{”.

Your workspaceTypeScript
// Parse to unknown, validate the count, then return a truthful result variant.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Syntax validation and domain validation are different stages with different messages.

Hint 2: a first step

Use a narrow try/catch around JSON.parse, then numeric guards outside that block.

Show one solution
One possible solutionTypeScript
type Result =
  | {
      kind: "ok";

      count: number;
    }
  | {
      kind: "error";

      message: string;
    };

function parseCount(json: string): Result {
  let value: unknown;

  try {
    value = JSON.parse(json);
  } catch {
    return { kind: "error", message: "Invalid JSON" };
  }

  if (
    typeof value !== "number" ||
    !Number.isFinite(value) ||
    !Number.isInteger(value) ||
    value < 0
  ) {
    return { kind: "error", message: "Invalid count" };
  }

  return { kind: "ok", count: value };
}

function display(result: Result): string {
  return result.kind === "ok" ? String(result.count) : result.message;
}

console.log(display(parseCount("0")));
console.log(display(parseCount('"5"')));
console.log(display(parseCount("{")));

Enable JavaScript to edit and type-check this example.

0
Invalid count
Invalid JSON

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Test JSON null, an object, -1, 2.5 and true. Explain why the quoted numeric string is rejected even though Number(“5”) would work. This boundary validates a numeric JSON contract instead of silently coercing it.

Was this article helpful?

Your response is saved on this device.