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.
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.
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.
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.
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.
The rejected source
import {totalDuration} from "../src/core";
totalDuration(["2"]);
Actual TS2322 diagnostic
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
{
"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
"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: "" };
}