CodeRigor

LESSON

Write a small generic and use a local compiler

Keep input/output type relationships with a generic mapping helper and verify real strict compiler diagnostics.

A generic expresses a relationship between types without replacing them with any. mapValues takes T values and a callback producing U, then returns U values. The compiler can infer T and U from an ordinary call, while still rejecting a result assigned to the wrong type.

The downloadable lab includes a locked TypeScript6.0.3 compiler dependency and explicit compiler settings. It checks both working programs and intentionally invalid fixtures. A negative fixture must produce the expected diagnostic; a failing unrelated setup command does not count as a successful type test.

Write a small generic and use a local compiler
start / 3
Keep input/output type relationships with a generic mapping helper and verify real strict compiler diagnostics.
Write a small generic and use a local compiler
Keep input/output type relationships with a generic mapping helper and verify real strict compiler diagnostics.

The callback receives both value and index. The result array is U[], so a callback returning text produces text values and one returning lengths produces numbers. Iterate dense array entries in order and call the transform once per value.

The local compiler is invoked with fixed flags and noEmitOnError. A rejected program should not leave a runnable emitted artifact. Type-level failures and runtime test failures are different feedback channels; the included check command prints them separately.

TypeScript
export function mapValues<T, U>(values: readonly T[], transform: (value: T, index: number) => U): U[] {
    const result: U[] = [];
    for (const [index, value] of values.entries()) {
        result.push(transform(value, index));
    }
    return result;
}
console.log(JSON.stringify(mapValues([2,3], (value,index) => `${index}:${value*2}`)));
console.log(JSON.stringify(mapValues(["api","db"], value => value.length)));

Generics preserve useful information but do not deep-clone payloads. mapValues returns a new array; its callback decides what each element becomes. A thrown callback error should propagate rather than silently becoming an undefined result.

The bundled negative checker verifies real TS2322/TS2345/TS18046-style diagnostics under the pinned compiler. The README names the exact commands and cleanup. Installation is a local setup step; hosted jobs already have the compiler and never fetch packages.

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
function firstOr<T>(values: readonly T[], fallback: T): T {
    return values[0] ?? fallback;
}
console.log(firstOr([0,2], 99));
console.log(firstOr<string>([], "missing"));

Implement the generic helper and run the downloadable positive/negative compiler checks. Keep callback behavior and type relationships separate. Next migrate the CLI and compare actual process contracts.

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 →