# Smooth Operators: Declarative Sequence Transformations in TypeScript

# Sequence Operators

> In this post, we will refer to these higher-order array methods as *sequence operators*, since they operate over sequences of values in a declarative way.
> 
> You can find the accompanying example code on GitHub: [Declarative Sequence Transformations](https://github.com/oneirosoft/oneiro-blog/tree/main/examples/declarative-sequence-transformations)

A *sequence operator* is a higher-order function that takes a sequence, like an array or list, applies a function to each element, and returns a new value or sequence, enabling declarative, step-by-step data transformations. Many modern languages provide their own versions of these operators, such as LINQ in C# or the Stream API in Java. In this post, we will focus on several core sequence operators using TypeScript as our example.

Throughout this article, we will make use of a Note type. Assume we already have data coming from some data source, and we will refer to it simply as notes. The aim here is to keep the article abstract and focused on transformations rather than mock data, so you will often see a variable called `notes` in the examples.

## Type Reference

`note.types.ts`

```typescript
interface Note {
	id: string
	title: string
	tags: string[]
	sections: Section[]
}

interface Section {
	heading: string
	links: string[]
	references: string[]
}
```

## `map(T => R)`

`map` transforms each element in a sequence using a function and returns a new sequence of the same length. Each element of the sequence is passed to the mapping function.

We can apply a transformation to each element in the notes sequence to produce cards by extracting data from each note and reshaping it into a new type.

```typescript
interface Card {
	id: string
	title: string
	sectionCount: number
	tagCount: number
}

const cards: Card[] = 
	notes.map<Card>(note => ({
		id: note.id,
		title: note.title,
		sectionCount: note.sections.length,
		tagCount: note.tags.length
	}))
```

The result is a new sequence where each Note has been transformed into a `Card`. The original `notes` array is left untouched, and a new cards array is produced with the same number of elements, but a different data shape.

## `filter(T => boolean)`

`filter` evaluates each element in a sequence using a predicate function, that is, a function that returns a boolean. `filter` does not transform items in the list. Instead, it pares down the sequence such that the resulting sequence only contains elements for which the predicate evaluates to `true`.

We can apply a predicate to each element in the `notes` sequence to produce a new sequence of only those notes whose tags include `'functional'`.

```typescript
const functionalNotes: Note[] = 
	notes.filter(note => note.tags.includes('functional'))
```

We can also select out any notes that have sections.

```typescript
const isNotEmpty = <T>(arr: T[]) => arr.length > 0

const notesWithContent: Note[] = 
	notes.filter(note => isNotEmpty(note.sections))
```

In the examples above, we pare down the original notes sequence based on the provided predicates. In the first example, we only keep notes that include a tag equal to "functional". In the second example, we only keep notes whose `sections` sequence is not empty.

## `flatMap(T => R[])`

`flatMap` is very similar to `map` in that it applies a transformation to a sequence. The difference is that the provided function returns another sequence, which is then flattened down into a single sequence of elements.

We can use `flatMap` to grab all of the tags within each note and flatten those tags into a single sequence, without touching the original array.

```typescript
const allTags: string[] = 
	notes.flatMap<string>(note => note.tags)
```

We can use the same concept to extract all links across all notes. Feel free to jump to the [Type Reference](https://blog.oneiro.dev/smooth-operators-declarative-sequence-transformations-in-typescript#heading-type-reference) section as a refresher on the shape of the `Note` type.

```typescript
const allSections: Section[] = 
	notes.flatMap<Section>(note => note.sections)
	
const allLinks: string[] =
	allSections.flatMap<string>(section => section.links)
```

Now, let’s extend the example beyond simple extraction of strings and indulge in transforming notes to provide us with an entirely new sequence of link artifacts.

```typescript
interface LinkArtifact {
    noteId: string
    heading: string
    url: string
}

const allLinkArtifacts: LinkArtifact[] =
    notes.flatMap(note =>
        note.sections.flatMap(section =>
            section.links.map(link => ({
                noteId: note.id,
                heading: section.heading,
                url: link
            }))
        )
    )
```

In the examples above, we are transforming a sequence of notes into various data shapes by extracting nested sequences, flattening them, extracting additional values, and then flattening again. In other words, we are applying a transformation over a sequence of sequences and flattening that down into a single sequence, conceptually: `[[]] -> []`.

Conceptually, `flatMap` can be hard to grasp. If it helps, focus on the simple example of extracting all tags from a sequence of notes and ending up with just a single sequence of tags in the end.

## `reduce((TState, TValue) => TState, TState)`

`reduce` is a sequence operator that applies a function to each value of a sequence and accumulates the result into a single state value. On each pass, the current element from the sequence and the previous accumulator value are provided to the reducer function. The value returned becomes the next accumulator. `reduce` requires a seed, or initial value, to kick things off.

The following example is for explanation, as we could more easily get the count of all tags by first applying `flatMap` and then accessing the resulting length. However, counting all items in a sequence is a simple example to grasp conceptually, and thus a good introduction to reduce.

```typescript
const allTags: string[] = 
	notes.flatMap(note => note.tags)

const tagCount: number =
	allTags.reduce((count: number, _tag: string) => count + 1, 0)
```

In the example above, we omit the use of the tag itself because we only care about the total count. For each element in the sequence, we add `1` to the current `count`, which becomes the next accumulator value. This continues until the end of the sequence is reached. You can think of this process as recursive in nature, since each step depends on the result of the previous one.

Now, let’s imagine that our goal is to count all of the individual unique tags in our notes and end up with a data shape that uses a `tag` as a key and its `count` as the value.

```typescript
type TagCounts = Record<string, number>

const allTags: string[] =
    notes.flatMap(note => note.tags)

const counts: TagCounts =
    allTags.reduce<TagCounts>((accumulator: TagCounts, tag: string) => {
        const currentCount = accumulator[tag] ?? 0
        accumulator[tag] = currentCount + 1
        return accumulator
    }, {})
```

In the example above, we first use flatMap to extract all tags from the notes sequence, producing a simple structure to work with, a single sequence of tags. We then reduce that sequence down into a single data structure, a record whose keys are tags and whose values represent how many times each tag appears in the sequence.

Finally, in the below example, we use reduce to accumulate multiple sequences into a single structured result. Rather than reducing to a number or a simple value, we are reducing the sequence of notes into a richer data shape that contains two sequences, one for links and one for references.

```typescript
interface Artifacts {
    links: string[]
    references: string[]
}

const artifacts: Artifacts =
    notes.reduce<Artifacts>((acc, note) => {
        const links =
            note.sections.flatMap(section => section.links)

        const references =
            note.sections.flatMap(section => section.references)

        return {
            links: acc.links.concat(links),
            references: acc.references.concat(references)
        }
    }, { links: [], references: [] })
```

We start with a seed value that represents an empty result, an object with two empty sequences. On each pass, we take a single note and extract its links and references by flattening over its sections. Rather than mutating the accumulator, we return a new object that combines the previously accumulated values with the newly extracted ones using concat. Each step produces a new accumulator that becomes the input to the next pass. By the end of the sequence, `reduce` has produced a final `Artifacts` value containing every link and every reference across all notes, without ever pushing to an external list or mutating shared state. `reduce` really shines here because it lets us describe how to build up a complex result from a sequence in a declarative way. In this example, we construct two sequences in parallel through the composition of `flatMap`, `concat`, and `reduce`.

## Building a sequence immutably

So far we have looked at `map`, `filter`, and `flatMap` as the building blocks for declarative transformations. We can combine those operators to build larger sequences without reaching for `reduce`, and without pushing into an external list.

Let’s say our goal is to build an artifacts sequence, but only for notes that include both the "functional" and "sequence" tags. From those notes, we want to extract all references and all links, then concatenate those results into a single list.

```typescript
interface Artifact {
    kind: 'link' | 'reference'
    value: string
}

const hasTag = (tag: string) =>
    (note: Note) =>
        note.tags.includes(tag)

const isFunctional: (note: Note) => boolean =
    hasTag('functional')

const isSequence: (note: Note) => boolean =
    hasTag('sequence')

const isFunctionalSequenceNote = (note: Note): boolean =>
    isFunctional(note) && isSequence(note)

const functionalSequenceNotes: Note[] =
    notes.filter(isFunctionalSequenceNote)

const references: Artifact[] =
    functionalSequenceNotes
        .flatMap(note =>
            note.sections.flatMap(section =>
                section.references.map(reference => ({
                    kind: 'reference',
                    value: reference
                }))
            )
        )

const links: Artifact[] =
    functionalSequenceNotes
        .flatMap(note =>
            note.sections.flatMap(section =>
                section.links.map(link => ({
                    kind: 'link',
                    value: link
                }))
            )
        )

const artifacts: Artifact[] =
    references.concat(links)
```

The above approach keeps each step focused and readable. `filter` narrows the input sequence down to only the notes we care about, `flatMap` flattens nested sequences like sections and links, and `map` transforms each extracted value into a consistent artifact shape. Finally, `concat` combines independently-built sequences into one larger sequence, all without mutating state or manually pushing into a list.

## Conclusion

In this post, we explored a small but powerful set of sequence operators and how they can be composed to transform data in a declarative and immutable way. Using `map`, `filter`, `flatMap`, and `reduce`, we shaped, narrowed, flattened, and accumulated sequences without mutating state or relying on external loops and push operations. Each operator focuses on a single concern, and when combined, they form a clear and expressive pipeline for working with data.

The examples built on the same `notes` sequence, showing how small, intentional transformations can grow into more meaningful results, from simple projections to richer artifact structures. Rather than describing how to perform each step imperatively, we described what each step should produce, letting the sequence operators handle the mechanics.

These operators only scratch the surface of what is available when working with sequences in JavaScript. Functions like `some` and `every` help answer questions about a sequence, `find` and `findIndex` locate specific elements, `sort` and `slice` help reshape and reorder data, and `groupBy` opens the door to building structured views of a sequence. When combined with chaining and composition, these operators provide a rich vocabulary for expressing data transformations clearly and concisely.

Once you start thinking in terms of sequences and transformations, you may find fewer reasons to reach for mutable loops and temporary collections. Instead, you can build results step by step, directly from the data, in a way that is easier to read, reason about, and extend.

Building a sequence is about the journey and not a mutable destination.
