Runtime Validation

The Course
•
5 min read

A record validator answers two questions: is this the right shape, and do its values meet the application’s rules? A minutes field can be a number and still be negative or infinite. A topic can be a string and still be blank.

JavaScript’s in operator checks whether a property is present. After narrowing a value to a non-null object, checking "topic" in value lets TypeScript track that property, initially as unknown. Then typeof establishes whether it is text. Repeat for every field promised by your domain type.

Order checks from broad to specific: reject non-objects and null; check property presence; check primitive types; then check domain restrictions. Return early when a requirement fails. This avoids reading a property before establishing that reading it is meaningful.

A validator can recognize an existing record, or a parser can construct a clean new one. Constructing a new record lets you deliberately normalize text and keep only relevant fields. Avoid a broad assertion that accepts unknown input without examining it.

This lesson’s guard promises only the fields and rules shown. Additional unrelated properties are allowed. If your application needs own-property-only checks or exact allowed keys, state and implement those rules separately. For imported JSON, prototype-related cases are less common, but tests should still establish the domain behavior you claim.

Read the checks from left to right

&& evaluates its right side only if the left side is true. || evaluates its right side only if the left side is false. This short-circuit behavior, introduced in JavaScript’s conditions lesson, makes the order important: first prove an object exists, then prove a property exists, then prove its type, then call its methods.

The following longer form has the same purpose as the compact guard below. Each successful check supplies evidence for the next statement. Number.isInteger, from JavaScript input validation, additionally rejects fractions; Number.isFinite rules out NaN and infinity.

Trace one property guardTypeScript
function hasTopic(value: unknown): boolean {
  if (typeof value !== "object" || value === null) {
    return false;
  }

  if (!("topic" in value)) {
    return false;
  }

  if (typeof value.topic !== "string") {
    return false;
  }

  return value.topic.trim() !== "";
}

console.log(hasTopic(null));
console.log(hasTopic({ topic: 25 }));
console.log(hasTopic({ topic: " Types " }));

Enable JavaScript to edit and type-check this example.

Check your prediction
false
false
true

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
type Session = {
  topic: string;

  minutes: number;
};

function isSession(value: unknown): value is Session {
  if (typeof value !== "object" || value === null) {
    return false;
  }

  return (
    "topic" in value &&
    typeof value.topic === "string" &&
    value.topic.trim() !== "" &&
    "minutes" in value &&
    typeof value.minutes === "number" &&
    Number.isFinite(value.minutes) &&
    value.minutes >= 0
  );
}

console.log(isSession({ topic: "Types", minutes: 0 }));
console.log(isSession({ topic: " ", minutes: 25 }));
console.log(isSession(null));

Enable JavaScript to edit and type-check this example.

Check your prediction
true
false
false

Read a real compiler error

This local example demonstrates the contract mismatch that untrusted input could hide.

Find and fix the type errorTypeScript
type Session = {
  topic: string;

  minutes: number;
};

const session = { topic: "Types", minutes: "25" };
const trusted: Session = session;

console.log(trusted.minutes);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Validate the incoming minutes rather than asserting the object is Session. This course rejects text minutes at a JSON-record boundary; a separate form parser may intentionally convert strings before creating a record.

Your turn

Exercise

Runtime Validation: practise

Define Book with title:string and pages:number. Write isBook(value: unknown): value is Book. Require a nonblank title and finite nonnegative integer pages. Print true for a zero-page book, false for fractional pages, and false for null.

Your workspaceTypeScript
// Check the object, both fields, and the page-count rules.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Do not stop at typeof pages === “number”. The brief also requires an integer and a lower bound.

Hint 2: a first step

Reject null and non-object inputs before using the in operator.

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

  pages: number;
};

function isBook(value: unknown): value is Book {
  if (typeof value !== "object" || value === null) {
    return false;
  }

  return (
    "title" in value &&
    typeof value.title === "string" &&
    value.title.trim() !== "" &&
    "pages" in value &&
    typeof value.pages === "number" &&
    Number.isFinite(value.pages) &&
    Number.isInteger(value.pages) &&
    value.pages >= 0
  );
}

console.log(isBook({ title: "Notes", pages: 0 }));
console.log(isBook({ title: "Notes", pages: 2.5 }));
console.log(isBook(null));

Enable JavaScript to edit and type-check this example.

true
false
false

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 missing title, missing pages, a blank title, text pages, Infinity and -1. Then remove one guard condition temporarily and find a test case that exposes the defect. Explain why a predicate annotation did not prove the validator itself correct.

Was this article helpful?

Your response is saved on this device.