A type guard answers whether a value matches a shape; a decoder can return a normalized new value or a structured issue. type Decoder<T> = (value:unknown) => Result<T> expresses that reusable boundary.
Our decoder trims a title and validates a finite nonnegative integer page count. It does not coerce text counts, and it does not use assertions to pretend the input is valid. Constructing a new record also leaves unrelated input properties outside the domain value.
The compiler checks that a declared success has the required type. It does not prove your predicate conditions are complete or your normalization rule is correct. Test missing fields, null, wrong primitives, blank text, negative/fractional counts and zero.
Keep the result vocabulary from Day 3. A path identifies the failed field; a message explains its rule. These paths are simple human-readable field locations for this fixed schema, not a universal JSON-path query language.
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.
type Result<T> =
| {
ok: true;
value: T;
}
| {
ok: false;
issue: {
path: string;
message: string;
};
};
type Decoder<T> = (value: unknown) => Result<T>;
const text: Decoder<string> = value => {
if (typeof value !== "string" || value.trim() === "") {
return {
ok: false,
issue: { path: "text", message: "Expected nonblank text" }
};
}
return { ok: true, value: value.trim() };
};
const result = text(" Types ");
console.log(result.ok ? result.value : result.issue.message);Enable JavaScript to edit and type-check this example.
Check your prediction
Types Read the compiler error
A success result promises a number, but no numeric evidence has been established.
type Result<T> =
| {
ok: true;
value: T;
}
| {
ok: false;
message: string;
};
function decode(value: unknown): Result<number> {
return { ok: true, value };
}Enable JavaScript to edit and type-check this example.
Check your diagnosis
Narrow and validate value before constructing success. Casting would not implement the decoder.
A decoder returns evidence and a useful new value
A decoder can do more than answer whether the input is valid. It can normalize allowed text, construct a clean record and explain a failure. Keep that behavior separate from a type assertion, which performs no inspection or transformation.
A declared Decoder<T> is a promise made by the implementation. Runtime tests must challenge the conditions that support it. Include zero and empty input deliberately, because broad truthiness checks often reject valid data or accept invalid defaults.
type Result<T> =
| {
ok: true;
value: T;
}
| {
ok: false;
message: string;
};
type Decoder<T> = (input: unknown) => Result<T>;
const text: Decoder<string> = input =>
typeof input === "string" && input.trim() !== ""
? { ok: true, value: input.trim() }
: { ok: false, message: "Blank or nontext" };
const result = text(" Types ");
console.log(result.ok ? result.value : result.message);
console.log(text(" ").ok);Enable JavaScript to edit and type-check this example.
Check your prediction
Types
false The accepted output differs from the padded input. Validation established the rule; projection returned the normalized value. Those are real runtime steps beyond declaring string.
function validCount(input: unknown): input is number {
return (
typeof input === "number" &&
Number.isFinite(input) &&
Number.isInteger(input) &&
input >= 0
);
}
for (const input of [0, -1, 1.5, "0"]) {
console.log(validCount(input));
}Enable JavaScript to edit and type-check this example.
Check your prediction
true
false
false
false Each negative fixture targets a different rule. Number alone does not encode finite, integer or nonnegative, so the implementation needs all of them.
Try the rule on new inputs
Write a table mapping invalid container, missing field and invalid field value to a useful failure. Verify normalization and nonmutation for success rather than checking only the ok flag.
Your turn
Decoder Contracts: practise
Write a Decoder<number> for finite nonnegative integers. A bad input returns issue path pages and message Invalid count. Print 0 and pages for valid zero and invalid text.
// Unknown must be narrowed before numeric checks.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
The result contract should describe both valid data and a useful failure.
Hint 2: a first step
Use typeof, Number.isFinite, Number.isInteger and a lower-bound check.
Show one solution
type Result<T> =
| {
ok: true;
value: T;
}
| {
ok: false;
issue: {
path: string;
message: string;
};
};
type Decoder<T> = (value: unknown) => Result<T>;
const pages: Decoder<number> = value => {
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
!Number.isInteger(value) ||
value < 0
) {
return { ok: false, issue: { path: "pages", message: "Invalid count" } };
}
return { ok: true, value };
};
const a = pages(0);
const b = pages("0");
console.log(a.ok ? a.value : a.issue.message);
console.log(b.ok ? "unexpected" : b.issue.path);Enable JavaScript to edit and type-check this example.
0
pagesExplain 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
Add runtime assertions for negative, fractional, non-finite and missing values. Temporarily remove the integer check and verify a fractional test fails even though the decoder still type-checks.
Completion is saved in this browser only. It does not measure mastery.