Declarative Thinking
Saying *what* you want, not *how* to compute it — the mental shift that makes functional code small and readable.
The most important shift you'll make in this course is not learning a new operator or library. It is learning to think in a different shape. Imperative code says: do this, then this, then this. Declarative code says: the answer is described by this expression.
Both styles are valid. Both can solve the same problem. But once you internalize the declarative shape, code becomes dramatically smaller, easier to read, and easier to change.
The same task, both styles
Problem. From a list of orders, keep only the paid ones over $50, take their customer names, deduplicate, and sort alphabetically.
Imperative
This works. It is also full of mechanical bookkeeping: an index
variable, a seen set, a for loop, an explicit mutation, an
in-place sort. To read it, you have to execute it in your head
step by step.
Declarative
The declarative version reads like the English sentence we started
with: from orders, filter to paid > 50, take the customer, dedupe,
sort. There's no index, no mutation, no seen set, no for loop.
What "declarative" actually means
Declarative code:
- Describes the answer, not the procedure that produces it.
- Uses named transformations (
filter,map,reduce,groupBy) instead of generic control flow (for,while,if). - Composes — each step is a value that flows into the next.
- Avoids intermediate mutable state — no
let acc = ...; acc = acc + 1.
You can think of it as moving up one level of abstraction: instead of working at the level of statements, you work at the level of pipelines of values.
Why this matters: density and locality
Imperative code spreads the meaning of an operation across many
lines. A for loop with a conditional and an accumulator is five
or six syntactic elements to express one idea ("the sum of the
even squares"). Declarative code packs that idea into a single
expression you can read in one breath.
That has two consequences:
- Less code to write. Less code means fewer places for bugs.
- Locality of meaning. The whole pipeline is visible in one spot. To understand it, you don't have to scroll. You don't have to track variables. The pipeline is the meaning.
The vocabulary: a small set of transformations
Most of declarative data work is done with a handful of combinators. We'll use them throughout the course.
| Operator | Type sketch | What it does |
|---|---|---|
map | (A => B) => Array<A> => Array<B> | apply f to each element |
filter | (A => boolean) => Array<A> => Array<A> | keep elements that pass |
reduce | ((acc, A) => acc) => acc => Array<A> => acc | fold into one value |
flatMap | (A => Array<B>) => Array<A> => Array<B> | map then flatten |
find | (A => boolean) => Array<A> => A | undefined | first match |
some / every | (A => boolean) => Array<A> => boolean | existence quantifiers |
group / groupBy | (A => K) => Array<A> => Map<K, Array<A>> | bucket by key |
sort / sortBy | (A => key) => Array<A> => Array<A> | order |
zip | Array<A>, Array<B> => Array<[A, B]> | pair element-wise |
Notice what's not on this list: for, while, index,
accumulator, mutable variable. Declarative code does not need
them.
Building a pipeline, one stage at a time
Pipelines work best when each stage does one thing. Here is a realistic example — computing per-customer paid totals.
Each stage is a named value (paid, byCustomer, ranked). You
could comment out the last line and still see what the middle
results look like. The whole thing is a pipeline of values, not a
sequence of steps that mutate a shared state.
Declarative isn't only about arrays
The mental shift applies far beyond list processing.
Configuration: data, not code
The validation rules are data. The engine that interprets them is a small, generic function. Adding a new field is adding an entry to the data, not writing a new procedure.
Routing, queries, layouts, animations…
The same pattern recurs everywhere good software is written:
- Routing. Express a route as data (
{ path: "/users/:id", handler: ... }) and let a generic engine dispatch. - SQL. "What rows do I want?" not "loop the table and check each row".
- CSS. "Boxes that match
.button.primarylook like this." Not "find boxes, paint them". - Animations. Keyframes describe what should be true when, not how to move the screen each frame.
Declarative thinking is the deepest unifying idea in modern software. Functional programming is the discipline that makes it the default.
A multi-file challenge
The starter stats.ts contains an imperative function
that computes the average value per category from a list of sales.
Reimplement avgPerCategory in a declarative pipeline style:
chained filter / reduce / map (no for, no let,
no push). The output must match exactly.
Expected output
books: 25
food: 8.5
toys: 14
Which of these best captures the essence of declarative code?
Code that uses arrow functions instead of function keyword
Code that has no if statements
Code that describes what you want as an expression over data, rather than how to compute it as a sequence of mutating steps
Code that uses TypeScript classes