Typorithm · easy

5. Control-flow narrowing and honest guards

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

5. Control-flow narrowing and honest guards

0%

Concept and contract

Use runtime evidence to refine types without making promises that the guard does not verify.

Narrowing follows runtime branches and removes impossible alternatives from a value’s declared type. typeof handles primitives; instanceof checks a constructor relationship; in checks property presence. The compiler uses these observations along reachable control-flow paths rather than rewriting the original declaration.

Start an external boundary at unknown. Unlike any, unknown permits storage but requires evidence before operations. A trustworthy guard checks every property promised by its predicate, including null and array cases when those distinctions matter to the target model.

Mechanism and worked example

The example first excludes null, because JavaScript reports typeof null as object.

The example first excludes null, because JavaScript reports typeof null as object. The in operator proves that title is present; another typeof check proves that its value is a string. Only after both checks does the predicate claim the Title shape.

The inferred refinement inside the caller depends on the predicate’s declared promise. TypeScript checks the guard’s body as ordinary code, but does not prove that returning true establishes the predicate. Review a user-defined guard as boundary code, not as a self-verifying theorem.

Complete worked example

typescript

type Title = { title: string };
function isTitle(value: unknown): value is Title {
  return typeof value === "object" && value !== null
    && "title" in value && typeof value.title === "string";
}
function title(value: unknown): string { return isTitle(value) ? value.title : "Missing"; }
Reasoning
The checks establish object presence and the precise field type before returning true.

Counterexamples and common mistakes

A truthy check rejects valid empty strings and numeric zero.

A truthy check rejects valid empty strings and numeric zero. When absence is the concern, use an explicit nullish or typeof check. When nonempty text is a business requirement, enforce that separately and document it instead of confusing it with a primitive type test.

A guard returning true unconditionally can make unsafe code compile. Assertions and predicates both create trust boundaries. Keep them small, cover malformed objects with runtime tests, and avoid claiming recursive validation after inspecting only one top-level property.

Rejected counterexample: expected compiler diagnostic

typescript

const raw: unknown = { title: "Engineer" };
// @ts-expect-error Unknown input has not been checked.
raw.title;
Reasoning
unknown prohibits property access until runtime evidence narrows the value.

Independent practice

Implement isSalary for an unknown value containing amount and currency.

Implement isSalary for an unknown value containing amount and currency. Require a finite nonnegative number and a nonempty currency string. Reject null, missing properties, strings masquerading as numbers and Infinity. Preserve zero as a valid amount.

Write the guard before reading the supplied solution. Use the predicate in a formatting function to demonstrate safe access. Include hostile but ordinary JSON shapes, such as a salary object with amount null, rather than testing only the successful record.

Step 1: Exclude null and nonobjects. Step 2: Check amount and currency independently.

Practice starter: solve before reading the solution

typescript

type Salary = { amount: number; currency: string };
function isSalary(value: unknown): value is Salary { throw new Error("TODO"); }
Reasoning
Test invalid field types and valid zero separately.
  • Hint 1: Zero is valid; truthiness is not the amount test.
  • Hint 2: Use finite and nonnegative checks after typeof.

Solution, tests and trade-offs

The solution verifies the exact facts its predicate promises and adds the stronger amount constraints at runtime.

The solution verifies the exact facts its predicate promises and adds the stronger amount constraints at runtime. The structural type alone cannot express finite nonnegative numbers, so callers should understand that these invariants are established by this particular validator.

Test zero and a regular positive amount, then reject each malformed case independently. A failing case should identify which boundary assumption changed. An object may still have extra fields; accepting them is a deliberate policy here, not proof of an exact serialized schema.

Practice solution and acceptance checks

typescript

type Salary = { amount: number; currency: string };
function isSalary(value: unknown): value is Salary {
  return typeof value === "object" && value !== null
    && "amount" in value && typeof value.amount === "number"
    && Number.isFinite(value.amount) && value.amount >= 0
    && "currency" in value && typeof value.currency === "string"
    && value.currency.trim().length > 0;
}

// Acceptance checks
if(!isSalary({amount:0,currency:"USD"}))throw new Error("zero");for(const x of [null,{}, {amount:"2",currency:"USD"},{amount:Infinity,currency:"USD"},{amount:-1,currency:"USD"},{amount:1,currency:" "}])if(isSalary(x))throw new Error("invalid");
Reasoning
The guard validates all promised fields and stronger numeric business constraints.

Interview reasoning and next step

Explain the difference between checking, narrowing and asserting.

Explain the difference between checking, narrowing and asserting. Give an example where typeof is insufficient and one where truthiness rejects a valid value. Discuss whether the guard accepts class instances, arrays or objects with inherited properties under its chosen contract.

Continue with nullability and indexed access. Narrowing is useful only when the declared types expose uncertainty honestly; suppressing undefined globally removes the very signal that tells you a branch or validation step is needed.

Optional stretch — after the core task works: Add an explicit supported-currency vocabulary.

Use runtime evidence to refine types without making promises that the guard does not verify.