Array Decoders

The Course
•
5 min read

A generic array decoder applies one Decoder<T> to every input element and returns Result<readonly T[]>. If the outer value is not an array, return a list issue. If any element fails, reject the whole list and prefix the issue path with its index.

Array.isArray narrows to any[] in TypeScript’s standard library. Immediately bind each element to unknown before invoking domain logic; do not let that historical any become permission to read unchecked properties. The generic decoder only promises the values its element decoder has actually produced.

array.entries() supplies pairs of index and value. Destructure those pairs to build locations like 1.pages. This is a normal JavaScript array method; the tuple syntax from Day 2 explains why each position has a different role.

Do not return partial accepted data while claiming a full-list success. If partial acceptance is desired, design a different result that carries both accepted values and issues. This course chooses all-or-error for a small clear contract.

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>;

function arrayOf<T>(decode: Decoder<T>): Decoder<readonly T[]> {
  return value => {
    if (!Array.isArray(value)) {
      return { ok: false, issue: { path: "list", message: "Expected array" } };
    }

    const output: T[] = [];

    for (const [index, element] of value.entries()) {
      const item: unknown = element;
      const result = decode(item);

      if (!result.ok) {
        return {
          ok: false,
          issue: {
            path: `${index}.${result.issue.path}`,
            message: result.issue.message
          }
        };
      }

      output.push(result.value);
    }

    return { ok: true, value: output };
  };
}

const text: Decoder<string> = value =>
  typeof value === "string"
    ? { ok: true, value }
    : { ok: false, issue: { path: "text", message: "Expected text" } };
const result = arrayOf(text)(["Types", 25]);

console.log(result.ok ? result.value.join(",") : result.issue.path);

Enable JavaScript to edit and type-check this example.

Check your prediction
1.text

Reject a whole list without leaking partial success

A list decoder combines a container rule with an element decoder. It should return success only after every element has passed. Returning the first accepted prefix under a success type would claim a complete result the implementation did not produce.

An index in an issue path tells the caller where a nested failure occurred. Preserve the element decoder’s field information and prefix it with the row position. Empty lists need an explicit policy too: this course accepts them as a complete empty result.

Reject a whole list without leaking partial success: traceTypeScript
type Result<T> =
  | {
      ok: true;

      value: T;
    }
  | {
      ok: false;

      path: string;
    };

function texts(input: unknown): Result<readonly string[]> {
  if (!Array.isArray(input)) {
    return { ok: false, path: "list" };
  }

  const result: string[] = [];

  for (const [index, element] of input.entries()) {
    const item: unknown = element;

    if (typeof item !== "string") {
      return { ok: false, path: String(index) };
    }

    result.push(item);
  }

  return { ok: true, value: result };
}

const result = texts(["A", 25, "B"]);

console.log(result.ok ? result.value.join(",") : result.path);

Enable JavaScript to edit and type-check this example.

Check your prediction
1

The accumulated A remains internal and is never returned as complete success after row 1 fails. Rebinding element to unknown avoids unchecked use of Array.isArray’s historical any[] narrowing.

Reject a whole list without leaking partial success: contrastTypeScript
const fieldIssue = { path: "pages", message: "Expected integer" };
const row = 2;
const issue = { ...fieldIssue, path: `${row}.${fieldIssue.path}` };

console.log(issue.path);
console.log(fieldIssue.path);

Enable JavaScript to edit and type-check this example.

Check your prediction
2.pages
pages

The wrapper enriches the path without mutating the nested decoder’s issue. That makes the same element decoder reusable inside different containers.

Try the rule on new inputs

Test an empty list, wrong outer container and invalid first, middle and last elements. Assert that no case returns a partial prefix as success. Keep the path policy consistent for every position.

Your turn

Exercise

Array Decoders: practise

Write decodeStrings(value:unknown) returning a Result<readonly string[]>. Reject non-arrays at path list and non-string elements at their numeric index as text. Print A,B for valid input and 1 for [“A”,2]. Do not coerce numbers into strings.

Your workspaceTypeScript
// This specialization prepares you to recreate the generic wrapper from memory.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Validate the container and every element before returning trusted output.

Hint 2: a first step

Use value.entries(), an unknown element binding, and a new string[] accumulator.

Show one solution
One possible solutionTypeScript
type Result<T> =
  | {
      ok: true;

      value: T;
    }
  | {
      ok: false;

      issue: {
        path: string;

        message: string;
      };
    };

function decodeStrings(value: unknown): Result<readonly string[]> {
  if (!Array.isArray(value)) {
    return { ok: false, issue: { path: "list", message: "Expected array" } };
  }

  const output: string[] = [];

  for (const [index, element] of value.entries()) {
    const item: unknown = element;

    if (typeof item !== "string") {
      return {
        ok: false,
        issue: { path: String(index), message: "Expected text" }
      };
    }

    output.push(item);
  }

  return { ok: true, value: output };
}

const a = decodeStrings(["A", "B"]);
const b = decodeStrings(["A", 2]);

console.log(a.ok ? a.value.join(",") : a.issue.path);
console.log(b.ok ? "unexpected" : b.issue.path);

Enable JavaScript to edit and type-check this example.

A,B
1

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

Test null, an empty list and a bad first or last element. Then generalize the implementation to arrayOf without assertions. Explain how a dishonest element decoder would undermine the generic wrapper despite a correct type signature.

Was this article helpful?

Your response is saved on this device.