TypeScript checks relationships in source before JavaScript runs. It can catch passing a string to a numeric function, but it does not change the numeric representation or automatically validate data from a file. This course starts after the JavaScript JSON CLI and preserves its behavior while making contracts explicit.
The hosted exercises use a pinned offline compiler with strict checks, ES2022/CommonJS output, noUncheckedIndexedAccess, exactOptionalPropertyTypes and noEmitOnError. The compiler does not install packages or read a submitted tsconfig. Successful output executes in the existing isolated Node sandbox.
Annotate exported parameters and results where they express a useful contract. Let obvious local values be inferred: let total=0 already gives a number. readonly number[] explains that the function reads its argument without changing it.
Numbers still use JavaScript’s floating-point semantics. The function checks each value and the running sum with Number.isFinite. Types cannot distinguish every nonfinite number, and a caller may reach the function through JavaScript or an assertion.
export function totalDuration(values: readonly number[]): number {
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;
}
console.log(totalDuration([1,2,0]));
try { totalDuration([Infinity]); } catch { console.log("nonfinite rejected"); }A type error prevents emission in this setup. A thrown RangeError happens after successful compilation. The lesson’s compiler check and runtime build task distinguish those outcomes instead of treating both as a syntax error.
Type checking is not an execution sandbox or a proof of correctness. The existing OS and Node isolation still confines execution. The hosted compiler exposes a minimal console/Node bridge, not DOM types or arbitrary third-party dependencies.
const count = 3;
const label: string = "jobs";
console.log(label, count + 1);
console.log("annotations do not validate a JSON file");Implement the numeric contract and distinguish a rejected type from a runtime RangeError. Next model the objects and optional fields used around the report.
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: "" };
}