100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
TypeScript Deep Dive
50 minintermediate

Foundations Practice — Typing a Small App

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.

Analogy🏏Cricket
🏏 Think of it like cricket: Picture a team's analytics department building its own reusable toolkit of statistical procedures — a way to compute career averages across any format, a way to trace every delivery in a match down to ball level, a way to filter players by any attribute. Each procedure is written once, carefully, to work on any team's data and to be combined with the others. Just as the analysts build a library of general, composable procedures rather than re-deriving statistics by hand each match, you build a library of general, composable type utilities rather than re-writing type transformations each time. Just as a good procedure is correct, finishes in reasonable time, and works across datasets, a good type utility is correct, terminating, performant, and reusable. Just as the toolkit's real value is that any analyst can apply it to any match, your library's value is that any developer can apply it to any type. This reveals the exercise's spirit: invest in correct, reusable tools once, and reap their leverage everywhere — the analytics department's philosophy and the library author's alike.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: Think of a bowling coach building drills where success is judged not by whether a ball is bowled at all, but by whether it lands on a precise painted spot every time — the target itself is the test. Just as the drills live on one practice strip with the targets painted on, you use one types.ts module holding the utilities with a set of Expect assertions at the bottom. Just as the strictest target discipline is switched on so a delivery a fraction off the spot is counted a miss, strict mode ensures the type-level results are sound and precise. Just as these drills produce no match runs — their whole purpose is landing the ball exactly right — type utilities have no runtime behaviour; their output is purely a type. Just as the coach judges the drill by whether every ball hit its painted mark, you verify by whether every utility produces exactly the expected type. This reveals why the setup is arranged this way: when precision itself is the deliverable, the target assertions are the entire test.
bash
# 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.ts

Step 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Think of how an analytics department first builds its verification rig before trusting any new procedure — a reference dataset with known correct answers, so any new calculation can be checked against the known result immediately. Just as the reference rig lets analysts confirm a new procedure is correct before relying on it, the Equal/Expect harness lets you confirm a new type utility is correct before relying on it. Just as the foundational career-average and ball-tracing procedures are built first because everything else uses them, DeepReadonly and DeepPartial are built first as the recursive backbone. Just as a procedure verified against known answers can be trusted across all future matches, a utility verified by the harness can be trusted across all future types. This reveals why the harness and foundations come first: a verification rig and a few core procedures, established up front, make everything built afterward checkable and dependable.
typescript
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Consider two analytics procedures. One filters the squad to only players matching a criterion — keep only the bowlers, dropping everyone else — exactly as PickByValue keeps only properties whose values match a type and drops the rest. The other traces every possible route through a nested match structure — from tournament down to innings down to each delivery — producing a full list of addressable points, exactly as Paths produces every dotted route through a nested object. Just as the filtering procedure discards non-matching players by leaving them out, PickByValue discards non-matching keys by remapping them to never. Just as the route-tracing procedure recurses down every level of the match to enumerate every reachable point, Paths recurses down every level of the type to enumerate every reachable path. This reveals how the toolkit composes: filtering by criterion and tracing every route are exactly the kinds of deep, systematic operations that mapped filtering and recursive template construction make possible at the type level.
typescript
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Think of how the analytics toolkit's procedures combine into a complete workflow: trace a route to a specific statistic, then look up the actual value at that route, then reformat its label for display, then apply a freeze so the published figure cannot be altered. Each step uses a different procedure, but they chain because they were designed to. Just as the route-tracing and value-lookup procedures are companions — one enumerates paths, the other resolves the value at a path — Paths and Get are companions, one listing dotted paths and the other resolving the type at one. Just as the label-reformatting step transforms a name's format, the CamelToSnake utility transforms a key's format. Just as the analysts chain filtering, lookup, and freezing into one pipeline, you compose PickByValue, Get, and DeepReadonly into combined types. This reveals the payoff of a well-designed toolkit: independent procedures that chain into complete workflows, at the analytics desk and at the type level alike.
typescript
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Think of a bowler's accuracy drill judged entirely by whether every ball lands on its painted target — no batter, no runs, just the marks; if every delivery hits its spot, the drill is passed, full stop. Just as the drill needs no match to prove accuracy, you verify the library with tsc --noEmit, which type-checks every utility and every Expect assertion without producing output. Just as a single ball missing its painted mark fails the drill outright, the harness makes a wrong type result a compile error, so a clean compile is a complete proof that every utility produces exactly the expected type. Just as there are no runs to tally because target-hitting is the whole point, there is no runtime to run — these are type-level functions, so the compile is the test. This reveals why type utilities are verified this way: when the deliverable is a precise type, the compiler landing every assertion is the full and final proof of correctness.
bash
# 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.
Lesson 6 of 35
0% complete