CodeRigor

LESSON

Validate external data at runtime

Treat parsed JSON as unknown and construct a canonical Job only after checking its actual fields.

JSON parsing establishes JSON syntax, not the application schema. The CLI therefore stores JSON.parse output in an unknown variable and checks its real fields before constructing a Job. Do not write as Job and call that validation.

The accepted record contract is the same as the JavaScript course: nonblank service, state ok/failed and finite nonnegative numeric duration. Extra fields are ignored, service is trimmed and input is preserved. The resulting object has a deliberate canonical shape.

Validate external data at runtime
start / 3
Treat parsed JSON as unknown and construct a canonical Job only after checking its actual fields.
Validate external data at runtime
Treat parsed JSON as unknown and construct a canonical Job only after checking its actual fields.

First exclude null and arrays. JavaScript reports typeof null as object, and arrays also have object type. Then use property-existence and typeof checks to narrow each field. Number.isFinite avoids accepting NaN or infinity, while the nonnegative check retains zero.

Return only the canonical fields rather than spreading an untrusted input object. The function’s return type documents the constructed result, and tests verify the actual result. Type assertions are erased; they do not add these checks to emitted JavaScript.

TypeScript
export type JobState = "ok" | "failed";
export interface Job { service: string; state: JobState; duration: number; }
export interface Group { service: string; count: number; failed: number; totalDuration: number; }
export interface CLIResult { code: number; stdout: string; stderr: string; }

export function parseJob(value: unknown): Job {
    if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("invalid record");
    if (!("service" in value) || typeof value.service !== "string" || value.service.trim() === "") throw new TypeError("invalid service");
    if (!("state" in value) || (value.state !== "ok" && value.state !== "failed")) throw new TypeError("invalid state");
    if (!("duration" in value) || typeof value.duration !== "number" || !Number.isFinite(value.duration) || value.duration < 0) throw new TypeError("invalid duration");
    return { service: value.service.trim(), state: value.state, duration: value.duration };
}
const value: unknown = JSON.parse('{"service":" api ","state":"ok","duration":0}');
console.log(JSON.stringify(parseJob(value)));

A program can compile an assertion that a string is a number, then still observe typeof value as string at runtime. That is why the compilation check deliberately includes an assertion example. Compiler success and trustworthy external data are different claims.

The build task tests null, arrays, wrong field types, missing fields, negative/nonfinite numbers and normalized output. The CLI catches parser failures and converts them into its own stable line-numbered error result.

Keep the external-data boundary
start / 3
Type relationships are checked before execution. JSON fields still need actual runtime checks.
Keep the external-data boundary
Type relationships are checked before execution. JSON fields still need actual runtime checks.
TypeScript
const external: unknown = "not a number";
const asserted = external as number;
console.log(typeof asserted);
console.log("an assertion added no runtime check");

Implement canonical runtime validation and challenge it with values that a type assertion would accept unchecked. Next use a small generic and the local strict compiler.

Pinned compiler 6.0.3 · Actual observed results

Types reject source. Runtime checks reject data.

These six invalid programs were checked by the real compiler under the lab’s strict settings. Choose one to inspect its source and actual diagnostic. This is separate from the migrated CLI’s runtime behavior.

TypeScript sourcestrict checksJavaScript emission

The rejected source

TypeScript
import {totalDuration} from "../src/core";
totalDuration(["2"]);

Actual TS2322 diagnostic

Text
negative/contracts.ts(2,16): error TS2322: Type 'string' is not assignable to type 'number'.

Twelve baseline parity cases · Twenty-four actual Node processes

Choose a case to inspect the exact typed CLI result that matched the original JavaScript process, including stdout, stderr and exit status. Compiler success alone does not establish this parity.

job-report · exit 0

JSON
{
  "code": 0,
  "stdout": "[{\"service\":\"a\",\"count\":1,\"failed\":1,\"totalDuration\":0},{\"service\":\"z\",\"count\":2,\"failed\":1,\"totalDuration\":5}]\n",
  "stderr": ""
}
Inspect actual emitted JavaScript from the complete typed core
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.totalDuration = totalDuration;
exports.displayRows = displayRows;
exports.decideState = decideState;
exports.describeState = describeState;
exports.parseJob = parseJob;
exports.mapValues = mapValues;
exports.runCLI = runCLI;
function totalDuration(values) {
    let total = 0;
    for (const value of values) {
        if (!Number.isFinite(value) || value < 0)
            throw new RangeError("finite nonnegative duration required");
        total += value;
        if (!Number.isFinite(total))
            throw new RangeError("duration total overflow");
    }
    return total;
}
function displayRows(jobs) {
    return jobs.map(job => ({ service: job.service, label: job.label ?? job.service }));
}
function decideState(value) {
    if (value === "ok" || value === "failed")
        return { kind: "valid", state: value };
    return { kind: "invalid", reason: "state must be ok or failed" };
}
function describeState(decision) {
    switch (decision.kind) {
        case "valid": return "state: " + decision.state;
        case "invalid": return decision.reason;
        default: {
            const unreachable = decision;
            return unreachable;
        }
    }
}
function parseJob(value) {
    if (value === null || typeof value !== "object" || Array.isArray(value))
        throw new TypeError("invalid record");
    if (!("service" in value) || typeof value.service !== "string" || value.service.trim() === "")
        throw new TypeError("invalid service");
    if (!("state" in value) || (value.state !== "ok" && value.state !== "failed"))
        throw new TypeError("invalid state");
    if (!("duration" in value) || typeof value.duration !== "number" || !Number.isFinite(value.duration) || value.duration < 0)
        throw new TypeError("invalid duration");
    return { service: value.service.trim(), state: value.state, duration: value.duration };
}
function mapValues(values, transform) {
    const result = [];
    for (const [index, value] of values.entries()) {
        result.push(transform(value, index));
    }
    return result;
}
const usage = "usage: job-report [--failed]\n";
function runCLI(args, input) {
    if (args.length === 1 && args[0] === "--help")
        return { code: 0, stdout: usage, stderr: "" };
    if (!(args.length === 0 || (args.length === 1 && args[0] === "--failed")))
        return { code: 2, stdout: "", stderr: usage };
    const groups = new Map();
    for (const [index, line] of input.split(/\r?\n/).entries()) {
        if (line.trim() === "")
            continue;
        let row;
        try {
            const decoded = JSON.parse(line);
            row = parseJob(decoded);
        }
        catch {
            return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        }
        if (args[0] === "--failed" && row.state !== "failed")
            continue;
        const previous = groups.get(row.service) ?? { service: row.service, count: 0, failed: 0, totalDuration: 0 };
        const totalDuration = previous.totalDuration + row.duration;
        if (!Number.isFinite(totalDuration))
            return { code: 1, stdout: "", stderr: `invalid record at line ${index + 1}\n` };
        groups.set(row.service, { service: row.service, count: previous.count + 1, failed: previous.failed + (row.state === "failed" ? 1 : 0), totalDuration });
    }
    const ordered = [...groups.values()].sort((a, b) => a.service < b.service ? -1 : a.service > b.service ? 1 : 0);
    const result = mapValues(ordered, row => ({ service: row.service, count: row.count, failed: row.failed, totalDuration: row.totalDuration }));
    return { code: 0, stdout: JSON.stringify(result) + "\n", stderr: "" };
}
Download the source, locked compiler setup, diagnostics and CLI checks →

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →