Learn TypeFirst · Part 1
Immutability
Welcome to Learn TypeFirst — a hands-on series that takes you from "I use TypeScript" to "I think in types." Two course rules apply from day one: any, as, is and ! are banned, and every function gets a full type signature. We start with the habit underneath everything else: stop mutating data.
Why mutation hurts
A mutated value changes for everyone holding a reference to it — including code far away that never asked. That's what "spooky action at a distance" means in practice: a list renders wrong in one component because a sort call three files away reordered the array in place. The bug isn't where the symptom is, which is why these are the bugs you can't reproduce.
Immutable code makes a simple promise: a value, once created, never changes. "Changing" something means creating a new value. Anyone holding the old one is unaffected, forever.
The five rewrites
These are the actual exercises from the course repo. Each is a function you've written a hundred times, in its mutating form — your job is the immutable rewrite.
1. Transforming an array — map, not index assignment:
// ✗ mutates the caller's array
function doubleArray(arr: number[]): number[] {
arr.forEach((value, index) => { arr[index] = value * 2 })
return arr
}
// ✓ returns a new array, caller's data untouched
function doubleArray(arr: number[]): number[] {
return arr.map((value) => value * 2)
} 2. Accumulating — reduce, not let. A let is a small mutation: a binding that changes over time.
function sumArray(arr: number[]): number {
return arr.reduce((sum, value) => sum + value, 0)
} 3. Appending — spread, not push:
function pushArray(arr: number[], value: number): number[] {
return [...arr, value]
} 4. Dropping the head — destructure, don't shift:
function tailArray(arr: number[]): number[] {
const [, ...tail] = arr
return tail
} 5. Updating an object — object spread, not assignment:
type Author = { name: string; age: number }
function updateAge(author: Author, newAge: number): Author {
return { ...author, age: newAge }
} One subtlety the course flags: when converting an object from one shape to another, prefer building the new object field by field over spreading the old one — a spread silently carries along extra fields that the target type didn't ask for, and you want shape changes to be deliberate.
Don't rely on willpower
Discipline is a lint rule, not a virtue. TypeFirst codebases enforce immutability mechanically — eslint-plugin-functional's immutable-data rule turns arr[i] = x, obj.field = y, push, splice and friends into errors. TypeScript helps too: readonly fields, ReadonlyArray<T>, and you'll notice delete already barely compiles under strict settings.
Why does this matter so much for everything that follows? Because the whole TypeFirst frontend is built on one idea — state is a value, and every change creates a new state via a pure function. That only works if nothing else can reach in and mutate. Immutability isn't a style preference here; it's the foundation the runtime stands on.