Checkpoint: Book Decoder

The Course
•
4 min read

Implement decodeBook from unknown to a normalized Book or an Issue. Require nonblank id and title, and finite nonnegative integer pages. Accept zero pages. Return only modeled properties, and leave the source unchanged.

Use a fixed field-check order so failures are predictable: object, id, title, pages. A value failing several rules reports the first failed rule in that order. That is a public diagnostic policy, not proof the remaining fields were valid.

Test each missing field independently, wrong primitive types, blanks, decimals and negative counts. A decoder that passes only one happy-path example has not established its runtime claims. This checkpoint prepares the element decoder used by the capstone’s generic array boundary.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Use Check types first; execution is allowed only when the real compiler accepts the source.

Predict, then runTypeScript
const record: unknown = { id: " b1 ", title: " Types ", pages: 0 };

if (
  typeof record === "object" &&
  record !== null &&
  "id" in record &&
  typeof record.id === "string"
) {
  console.log(record.id.trim());
}

Enable JavaScript to edit and type-check this example.

Check your prediction
b1

Acceptance cases before you code

Write a small test plan before implementing book decoding and output shape. These cases extend the brief; they are not a second worked solution. Predict the expected behavior independently, then test through the public function. A good ordinary example and a good boundary example should catch different possible mistakes.

Case to tryWhat your result should demonstrate
Zero pages with padded stringsAccept and normalize
Text pages or a missing required fieldReject at the relevant field path
Extra private input propertyDo not copy it into success

After the core task passes, deliberately break one rule and identify the case that notices it. Restore the implementation and explain the failure in terms of the contract, not the number of assertions. If a case requires a concept you have not recalled, revisit the relevant lesson before opening the solution.

Your turn

Exercise

Checkpoint: Book Decoder: practise

Define Book and Result<T>, implement decodeBook, and display Types: 0 for valid trimmed input. For a text pages field, print pages. The returned object must exclude any extra private field.

Your workspaceTypeScript
// Validate each field, then construct the clean record explicitly.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

An accepted unknown must have evidence for every promised property.

Hint 2: a first step

Reject a non-null-object mismatch before testing fields with in.

Show one solution
One possible solutionTypeScript
type Book = {
  readonly id: string;

  readonly title: string;

  readonly pages: number;
};

type Result<T> =
  | {
      ok: true;

      value: T;
    }
  | {
      ok: false;

      issue: {
        path: string;

        message: string;
      };
    };

function decodeBook(value: unknown): Result<Book> {
  if (typeof value !== "object" || value === null) {
    return { ok: false, issue: { path: "book", message: "Expected object" } };
  }

  if (
    !("id" in value) ||
    typeof value.id !== "string" ||
    value.id.trim() === ""
  ) {
    return { ok: false, issue: { path: "id", message: "Invalid id" } };
  }

  if (
    !("title" in value) ||
    typeof value.title !== "string" ||
    value.title.trim() === ""
  ) {
    return { ok: false, issue: { path: "title", message: "Invalid title" } };
  }

  if (
    !("pages" in value) ||
    typeof value.pages !== "number" ||
    !Number.isFinite(value.pages) ||
    !Number.isInteger(value.pages) ||
    value.pages < 0
  ) {
    return { ok: false, issue: { path: "pages", message: "Invalid pages" } };
  }

  return {
    ok: true,
    value: {
      id: value.id.trim(),
      title: value.title.trim(),
      pages: value.pages
    }
  };
}

const a = decodeBook({ id: " b1 ", title: " Types ", pages: 0, private: true });
const b = decodeBook({ id: "b2", title: "Types", pages: "10" });

if (a.ok) {
  if ("private" in a.value) {
    throw new Error("Projection leaked");
  }

  console.log(`${a.value.title}: ${a.value.pages}`);
}

console.log(b.ok ? "unexpected" : b.issue.path);

Enable JavaScript to edit and type-check this example.

Types: 0
pages

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Write runtime assertions for every field boundary, including NaN and Infinity supplied directly. Add arrayOf and test a malformed second row. Preserve the first issue path and do not silently drop rejected rows.

Was this article helpful?

Your response is saved on this device.