You now have enough TypeScript to convert a small JavaScript application. The capstone is the console core of the beginner JavaScript study planner: import saved sessions and summarize completed work. A webpage is deliberately outside this beginner TypeScript project, so the new learning stays focused on types and boundaries.
Read the program in three layers. Session and Report name trusted data. parseSessions accepts text, parses to unknown, validates every row, and constructs new Session records. summarize accepts trusted readonly records and calculates the report without changing them. The caller narrows the import result before using its sessions.
The report preserves the JavaScript project’s rule that incomplete rows do not contribute completed minutes. The importer is a new, deliberately smaller contract: it accepts zero and finite nonnegative decimals, whereas the JavaScript beginner form required whole minutes from 1 through 240. Reuse the JavaScript validation policy instead if you want an exact migration of that form. TypeScript does not choose either rule for you. For this course’s importer, zero is valid; negative and non-finite minutes are not. Topics are trimmed and must not become blank. A bad row rejects the entire import rather than silently disappearing.
Array.isArray uses any[] in TypeScript’s standard library. The loop immediately binds each element to unknown so its properties still require guards. This is an intentional boundary discipline, not an assertion. Notice that the program never casts unknown data into Session.
The full runnable version is below. Read and predict it before running. It is longer than earlier examples because it connects the pieces, not because the type syntax is more advanced. You can download the modular project; the local-running lesson explains its files and commands.
Predict, check, then run
Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.
type Session = {
readonly topic: string;
readonly minutes: number;
readonly done: boolean;
};
type ImportResult =
| {
kind: "ok";
sessions: readonly Session[];
}
| {
kind: "error";
message: string;
};
// JSON syntax, field shape and domain rules are separate checks.
function parseSessions(json: string): ImportResult {
let value: unknown;
try {
value = JSON.parse(json);
} catch {
return { kind: "error", message: "Invalid JSON" };
}
if (!Array.isArray(value)) {
return { kind: "error", message: "Expected a list" };
}
const sessions: Session[] = [];
// Array.isArray narrows to any[] in TypeScript's library. Explicitly keep
// each external element unknown so no unchecked properties slip through.
for (const element of value) {
const item: unknown = element;
if (
typeof item !== "object" ||
item === null ||
!("topic" in item) ||
typeof item.topic !== "string" ||
item.topic.trim() === "" ||
!("minutes" in item) ||
typeof item.minutes !== "number" ||
!Number.isFinite(item.minutes) ||
item.minutes < 0 ||
!("done" in item) ||
typeof item.done !== "boolean"
) {
return { kind: "error", message: "Invalid session" };
}
sessions.push({
topic: item.topic.trim(),
minutes: item.minutes,
done: item.done
});
}
return { kind: "ok", sessions };
}
type Report = {
completedCount: number;
completedMinutes: number;
};
function summarize(sessions: readonly Session[]): Report {
let completedCount = 0;
let completedMinutes = 0;
for (const session of sessions) {
if (session.done) {
completedCount += 1;
completedMinutes += session.minutes;
}
}
return { completedCount, completedMinutes };
}
const saved =
'[{"topic":" Types ","minutes":25,"done":true},{"topic":"DOM","minutes":40,"done":false}]';
const result = parseSessions(saved);
if (result.kind === "error") {
console.log(result.message);
} else {
const report = summarize(result.sessions);
console.log(
`${report.completedCount} complete, ${report.completedMinutes} min`
);
}Enable JavaScript to edit and type-check this example.
Check your prediction
1 complete, 25 min Trace input into trusted data and then a report
parseSessions accepts text and must justify a typed success. summarize accepts already trusted sessions and should not parse JSON again. Trace one completed and one incomplete record with unequal durations. A completed zero-minute session still counts as a completed record, so count and duration need independent expectations.
| Boundary or case | What to inspect |
|---|---|
| Syntax | Malformed text fails before field decoding |
| Decoder | Reject wrong fields and normalize accepted topics |
| Report | Count completed records and sum their minutes without mutation |
Find both functions in model.ts and their caller in main.ts. Change only the fixture: no sessions, valid zero, text minutes or a blank topic. Predict which stage handles each case. Verify source records survive summary unchanged.
Your turn
Capstone: Study Planner: practise
Recreate the pure summarize part from memory. Define readonly Session fields and a Report. For completed Types/25 and incomplete DOM/40, print 1 complete, 25 min. Also print 0 complete, 0 min for an empty list.
// Rebuild the report function before adding import validation.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
Start with the trusted-data calculation. It should not parse JSON itself.
Hint 2: a first step
Use local counters and return a named report object.
Show one solution
type Session = {
readonly topic: string;
readonly minutes: number;
readonly done: boolean;
};
type Report = {
completedCount: number;
completedMinutes: number;
};
function summarize(sessions: readonly Session[]): Report {
let completedCount = 0;
let completedMinutes = 0;
for (const session of sessions) {
if (session.done) {
completedCount += 1;
completedMinutes += session.minutes;
}
}
return { completedCount, completedMinutes };
}
function display(report: Report): string {
return `${report.completedCount} complete, ${report.completedMinutes} min`;
}
console.log(
display(
summarize([
{ topic: "Types", minutes: 25, done: true },
{ topic: "DOM", minutes: 40, done: false }
])
)
);
console.log(display(summarize([])));Enable JavaScript to edit and type-check this example.
1 complete, 25 min
0 complete, 0 minExplain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.
Change it and explain it
Change the saved JSON in the full example to an empty list, malformed JSON, a blank topic and text minutes. Trace exactly which branch handles each. Identify which annotations could be inferred and which ones communicate a useful boundary. Do not weaken types to make invalid data pass.
Completion is saved in this browser only. It does not measure mastery.