Dataslope logoDataslope

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

Code Block
TypeScript 5.7

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

Code Block
TypeScript 5.7

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:

  1. Describes the answer, not the procedure that produces it.
  2. Uses named transformations (filter, map, reduce, groupBy) instead of generic control flow (for, while, if).
  3. Composes — each step is a value that flows into the next.
  4. 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.

OperatorType sketchWhat 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> => accfold into one value
flatMap(A => Array<B>) => Array<A> => Array<B>map then flatten
find(A => boolean) => Array<A> => A | undefinedfirst match
some / every(A => boolean) => Array<A> => booleanexistence quantifiers
group / groupBy(A => K) => Array<A> => Map<K, Array<A>>bucket by key
sort / sortBy(A => key) => Array<A> => Array<A>order
zipArray<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.

Code Block
TypeScript 5.7

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

Code Block
TypeScript 5.7

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.primary look 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

Challenge
TypeScript 5.7
Turn a loop into a pipeline

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

QuestionSelect one

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

On this page