Dataslope logoDataslope

Capstone — A Functional Data Pipeline

Combine everything from the course into a single, multi-file, end-to-end functional program.

This chapter is a small project that ties together almost every idea from the course. We'll build a complete program that:

  1. Parses raw input strings into a typed domain.
  2. Validates them with Result so all errors are first-class.
  3. Transforms the valid records with a pipe of pure functions.
  4. Aggregates the data using reduce.
  5. Reports the output through a tiny imperative shell.

There is no library involved; just plain TypeScript.

The scenario

We're given a list of raw "sale" rows from a CSV-like source:

"2024-09-01,A,3,12.50"
"2024-09-01,B,1,9.99"
"2024-09-02,A,2,12.50"
"bogus row"
"2024-09-02,C,4,2.00"

Each well-formed row is date,sku,qty,price. We want to:

  • skip bogus rows but report them,
  • compute the total revenue per SKU,
  • compute the date with the highest revenue,
  • print a small report.

We will solve it as a chain of pure functions; the shell will only hold the raw data and print the final report.

The core

Code Block
TypeScript 5.7

Run the example. Every function in parse.ts, transform.ts, and pipeline.ts is pure. Every type in types.ts is immutable. Every error is a value. The only impure code is the four console.logs in main.ts — and that's exactly where it should be: the edge of the program.

What patterns from the course you just used

Let's audit. In this single small program you used:

  • Pure functions (parseRow, withRevenue, sumByKey, maxByValue, partition, runReport).
  • Immutability (readonly everywhere; spread updates).
  • Discriminated unions (Result<E, A>).
  • Type-driven design (Sale and Priced types make illegal states unrepresentable; Result makes failures part of the signature).
  • Generic abstractions (sumByKey works for any element type).
  • Composition (runReport is partition → map(withRevenue) → sumByKey → maxByValue).
  • Functional core, imperative shell (pipeline.ts is the pure core; main.ts is the shell).

That's the entire course in one screen of code. None of it required a library or a framework. It only required thinking in the functional style.

Extending the capstone

Try these on your own:

  1. Add a filter for sales below a minimum price before aggregating. Where in the pipeline does it go? (Hint: between withRevenue and sumByKey.)
  2. Add a second report: top 3 SKUs by revenue. (Hint: sort the byProduct entries.)
  3. Make the pipeline async: change parseRow to a TaskResult<string, Sale> and have the shell run them in parallel. Notice how little the core changes — most of the work is on the boundary.
  4. Add branded types: make SKU a branded string, and have parseRow produce a Sale whose sku field has that brand. Now any function that takes an SKU is guaranteed to receive a parsed one.

Each extension is a small, mostly-pure change. That's the dividend of building this way.


QuestionSelect one

Looking at the capstone, what makes the program as a whole easy to test and evolve?

It uses ES modules

Every .ts file is small

All business logic lives in pure functions that take data and return data, with side effects confined to a tiny shell — so each piece can be tested with plain inputs, and the parts you'll want to change live where you expect them

The program is short

On this page