What You'll Build
In this exercise you will build a small, fully-typed command-line cricket scorecard application that ties together everything from Module 1: primitives, arrays, tuples, enums, interfaces, type aliases, and typed functions. The app models an innings — a list of players, the deliveries each faces, and the running total — and prints a formatted scorecard. The point is not the cricket logic itself but the discipline of letting the type system describe your data so precisely that whole categories of mistakes become impossible. By the end you will have a single TypeScript file that compiles under strict mode with no `any`, where every function has a clear contract, every shape is named, and the closed sets of outcomes are modelled as enums or literal unions. This is exactly the foundation on which every larger TypeScript project is built, scaled up only in size.
Prerequisites
- Node.js 18 or later installed, which provides the runtime to execute the compiled JavaScript and the npm package manager used for the TypeScript compiler.
- Comfort with primitive types, inference, and annotations from Lessons 01 and 02, since every variable in the app relies on them.
- Familiarity with arrays, tuples, and enums from Lesson 03, which model the innings list, the per-delivery pairs, and the closed outcome set.
- Understanding of interfaces and type aliases from Lesson 04 to name the player and scorecard shapes used throughout the app.
- Knowledge of typed functions, parameters, and return types from Lesson 05 to give every operation a precise, checked contract.
Setup & Project Structure
You will create a minimal TypeScript project: one source file, a `tsconfig.json` enabling strict mode, and the TypeScript compiler installed locally. Strict mode is essential here because it turns on null-safety and disallows implicit `any`, which is what forces the precise typing this exercise is about. The structure is intentionally tiny — a single `scorecard.ts` — so all your attention stays on the types rather than on project plumbing. You will compile with `tsc` and run the emitted JavaScript with Node, confirming both that the code type-checks and that it produces the expected scorecard output.
# Create and enter the project directory
mkdir cricket-scorecard && cd cricket-scorecard
# Initialise npm and install TypeScript locally
npm init -y
npm install --save-dev typescript
# Create a strict tsconfig.json
npx tsc --init --strict --target ES2020 --module commonjs
# Project layout:
# cricket-scorecard/
# ├── package.json
# ├── tsconfig.json
# └── scorecard.ts <-- you write all code here
# Create the source file
touch scorecard.tsStep 1 — Foundation
Step 1 establishes the data model: the named shapes and closed sets that everything else will use. You will define an enum for the closed set of dismissal modes, a type alias for the union of legal per-ball outcomes, an interface for a player, and a tuple type for a single delivery record pairing an outcome with the runs it produced. Modelling these first is deliberate — once the shapes exist, the compiler guides the rest of the build, rejecting any later code that violates them. This mirrors how real projects begin with the domain types before any behaviour is written.
// scorecard.ts — Step 1: the data model
// Closed set of dismissal modes (enum: a named, fixed vocabulary).
enum DismissalMode {
Bowled = "bowled",
Caught = "caught",
LBW = "lbw",
RunOut = "run-out",
NotOut = "not-out",
}
// Closed set of legal per-ball run outcomes (literal union: zero runtime cost).
type BallOutcome = 0 | 1 | 2 | 3 | 4 | 6 | "W"; // "W" = wicket
// A delivery is a fixed pair: (outcome, runs scored off it).
type Delivery = [outcome: BallOutcome, runs: number];
// A player's shape (interface: extensible, named).
interface CricketPlayer {
readonly id: number; // fixed once assigned
name: string;
deliveriesFaced: Delivery[]; // array of delivery tuples
dismissal: DismissalMode;
nickname?: string; // optional
}Step 2 — Core Logic
Step 2 builds the typed functions that compute statistics from the model. You will write a function to total a player's runs from their deliveries, one to count balls faced, and one to compute strike rate using a default parameter to avoid division by zero. Each function declares precise parameter and return types, so the compiler verifies they consume and produce exactly the shapes from Step 1. Because the deliveries are an array of tuples, the reduce callbacks are fully typed without annotation, demonstrating how good upstream modelling makes downstream logic both concise and safe.
// scorecard.ts — Step 2: typed statistics functions
// Total runs scored from a player's deliveries.
function totalRuns(player: CricketPlayer): number {
return player.deliveriesFaced.reduce((sum, [, runs]) => sum + runs, 0);
}
// Count of balls faced (each delivery is one ball).
function ballsFaced(player: CricketPlayer): number {
return player.deliveriesFaced.length;
}
// Strike rate = runs per 100 balls. Default guards against division by zero.
function strikeRate(player: CricketPlayer, balls: number = ballsFaced(player)): number {
if (balls === 0) return 0;
return (totalRuns(player) / balls) * 100;
}
// Whether the player is still at the crease.
function isNotOut(player: CricketPlayer): boolean {
return player.dismissal === DismissalMode.NotOut;
}Step 3 — Integration & Enhancement
Step 3 brings the pieces together: you will create the innings as an array of players, write a function that builds a formatted scorecard line for one player, and an overloaded summary function that returns a single line for one player but a full multi-line scorecard for an array. The overload demonstrates input-dependent return types from Lesson 05. The scorecard string assembles the computed statistics into a readable row, handling the optional nickname. This integration step is where the value of precise modelling becomes visible — every function composes cleanly because their contracts already agree.
// scorecard.ts — Step 3: formatting and overloaded summary
function formatLine(player: CricketPlayer): string {
const label = player.nickname ? `${player.name} "${player.nickname}"` : player.name;
const status = isNotOut(player) ? "*" : ` (${player.dismissal})`;
return `${label.padEnd(22)} ${String(totalRuns(player)).padStart(3)}` +
` (${ballsFaced(player)}b) SR ${strikeRate(player).toFixed(1)}${status}`;
}
// Overloads: one player -> one line; many players -> full scorecard.
function summary(player: CricketPlayer): string;
function summary(players: CricketPlayer[]): string;
function summary(arg: CricketPlayer | CricketPlayer[]): string {
if (Array.isArray(arg)) {
return arg.map(formatLine).join("\n");
}
return formatLine(arg);
}
// Build the innings (array of players, each with delivery tuples).
const innings: CricketPlayer[] = [
{ id: 1, name: "Rohit Sharma", dismissal: DismissalMode.Caught,
deliveriesFaced: [[1, 1], [4, 4], [0, 0], [6, 6], [1, 1]] },
{ id: 2, name: "Virat Kohli", nickname: "King", dismissal: DismissalMode.NotOut,
deliveriesFaced: [[4, 4], [2, 2], [1, 1], [4, 4], [6, 6], [1, 1]] },
{ id: 3, name: "MS Dhoni", dismissal: DismissalMode.RunOut,
deliveriesFaced: [[6, 6], [0, 0], [2, 2], [4, 4]] },
];
console.log("Scorecard\n=========");
console.log(summary(innings));Step 4 — Testing & Verification
Now compile and run the app to confirm it both type-checks under strict mode and produces the expected scorecard. The `tsc` step verifies there are no type errors; if it prints nothing, the types are sound. Then run the emitted JavaScript with Node to see the formatted output. Verifying both the compile and the run is the habit to keep: a clean compile proves correctness of contracts, and a correct run proves the logic does what you intended.
# Compile (type-checks under strict mode and emits scorecard.js)
npx tsc
# If tsc prints nothing, there are no type errors. Now run it:
node scorecard.js
# Expected output:
# Scorecard
# =========
# Rohit Sharma 12 (5b) SR 240.0 (caught)
# Virat Kohli "King" 18 (6b) SR 300.0*
# MS Dhoni 12 (4b) SR 300.0 (run-out)Warning: The most common error in this exercise is a strict-mode failure on the optional `nickname` property: writing `player.nickname.toUpperCase()` fails because `nickname` may be `undefined`. The fix is to handle the absent case first, as `formatLine` does with the `player.nickname ? ... : ...` check. If you instead silence it with `player.nickname!` (the non-null assertion), you reintroduce exactly the runtime crash strict mode was protecting you from.
Extension Challenge: Extend the model to track bowlers as well as batters by adding a `Bowler` interface (overs, maidens, wickets, economy) and a discriminated `Participant` union of batter and bowler. Then add an overload to `summary` that accepts a `Participant[]` and formats each according to its role. This pushes you into discriminated unions, which Module 2 covers in depth.
- Defining the data model first — enums, literal unions, interfaces, and tuples — lets the compiler guide every later step and reject violations automatically.
- Literal unions like BallOutcome give the same closed-set safety as enums with zero runtime output, ideal for values that need no runtime object.
- Tuples model fixed positional records such as a delivery's outcome-and-runs pair, and their element types flow into reduce callbacks without annotation.
- Typed functions with default parameters compose cleanly because their contracts agree, turning statistics into safe, concise arithmetic over the model.
- Overloads give one summary function input-dependent return types, returning a single line for one player and a full card for an array.
- Strict mode forces handling of optional properties before use, and silencing that with a non-null assertion reintroduces the very crash it prevents.