Decoder Contracts

The Course
•
5 min read

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.

Predict, then runTypeScript
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.

Find and fix the type errorTypeScript
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.

A decoder returns evidence and a useful new value: traceTypeScript
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.

A decoder returns evidence and a useful new value: contrastTypeScript
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

Exercise

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.

Your workspaceTypeScript
// 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
One possible solutionTypeScript
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
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

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.

Was this article helpful?

Your response is saved on this device.