An assertion function has a return contract such as asserts value is string. If it returns normally, the checker narrows that caller’s value. If the condition is false, the implementation must throw. This is different from writing value as string, which performs no runtime check.
The checker trusts the assertion signature; it does not prove the body justifies it. A function that returns without checking can therefore create unsafe access while still compiling. Keep assertion functions small, reviewable and covered by both accepted and rejected inputs.
The count exercise can establish runtime rules such as finite, nonnegative and integer, but its asserted number type does not encode those rules. The compiler still sees number; the body and tests establish the stronger domain meaning.
Choose the failure style for the caller. An assertion is convenient for an internal invariant that should stop execution when broken. A Result-returning decoder is usually clearer for expected invalid user data. The event gateway keeps decoders at its external boundary so a bad message is an ordinary result, not an exception disguised as a parser failure.
The explicit function declaration below also makes the assertion signature easy for the checker and reader to follow. After assertText returns, toUpperCase is available without a cast. Before it returns, the input remains unknown.
Predict, check, and run
Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.
function assertText(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected text");
}
}
const input: unknown = "Types";
assertText(input);
console.log(input.toUpperCase());Enable JavaScript to edit and type-check this example.
Check your prediction
TYPES Read the compiler error
Predict the rejected relationship before checking. Read the real diagnostic and its source location; fix the contract or the value instead of hiding it with an assertion.
function assertText(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected text");
}
}
const value: unknown = 25;
console.log(value.toUpperCase());Enable JavaScript to edit and type-check this example.
Check your diagnosis
Call the assertion before the property access. The function existing in the file does not narrow an unrelated expression automatically.
A trusted assertion body must justify its signature
The checker uses an assertion signature after a normal return. It does not audit whether the function body proved the claim. Keep the implementation small enough to inspect and give rejection tests the same attention as successful calls.
A domain check can be stronger than the narrowed type. Asserting number after validating a finite nonnegative integer still exposes only number to later code. If a distinct domain identity is important, the previous branded factory is another design option; do not imply that asserts encodes every predicate clause.
function assertText(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected text");
}
}
const input: unknown = " Types ";
assertText(input);
console.log(input.trim());Enable JavaScript to edit and type-check this example.
Check your prediction
Types The runtime check and signature agree. The caller can access string operations after the call without a value assertion.
// Deliberately dishonest: demonstrate why the body needs tests.
function badAssert(value: unknown): asserts value is string {}
const input: unknown = 25;
badAssert(input);
console.log(typeof input);Enable JavaScript to edit and type-check this example.
Check your prediction
number The checker now treats input as string, but runtime data stayed numeric. No conversion or validation happened. This counterexample should motivate a negative test, not a reusable helper to copy.
Try the rule on new inputs
Test null, a wrong primitive and blank text according to your exact assertion rule. Deliberately remove a body check and identify a test that notices the lie despite the unchanged signature.
Build it yourself
Assertion Functions: practise
Write assertCount(value) narrowing unknown to number only after checking a finite nonnegative integer. Print 1 after validating zero and adding one. Assert that text and a negative number throw; print rejected.
// Start from the stated contract. Add a valid call and an edge case.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
The signature says number; the body must establish the stronger count rule too.
Hint 2: a first step
Put each invalid call in try/catch, then check the rejection flag outside the try.
Show one solution
function assertCount(value: unknown): asserts value is number {
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
!Number.isInteger(value) ||
value < 0
) {
throw new Error("Invalid count");
}
}
const input: unknown = 0;
assertCount(input);
console.log(input + 1);
for (const bad of ["0", -1]) {
let rejected = false;
try {
assertCount(bad);
} catch {
rejected = true;
}
if (!rejected) {
throw new Error("Invalid input accepted");
}
}
console.log("rejected");Enable JavaScript to edit and type-check this example.
1
rejectedTrace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.
Change it and test it
Add NaN, Infinity and a fractional number. Compare the function with a Result decoder: which failure style better suits invalid form data? Explain why a lying assertion function undermines narrowing.
Completion is saved in this browser only. It does not measure mastery.