01/11Objects

LEARN TO PROGRAM · CHAPTER 07 · INTERMEDIATE

Objects, types and functional style

Programs grow. A hundred lines fit in your head; a hundred thousand don't. This chapter is about the three big ideas people use to keep large programs understandable: objects, which keep data and the code that guards it together; types, which describe the shape of data so a machine can check it; and functional style, which builds programs out of small functions that never change anything.

They are not rival teams. Good JavaScript and TypeScript use all three, often in the same file. In the 3D, every value is a glass box, and the ideas are about how the boxes connect.

Every code block runs in your browser. Edit it, break it, press Run.

01 · OBJECTS

Objects: values with names inside

So far, a value has been one thing: a number, a string. Real things have several parts. A dog has a name and an age; an order has items, a total and a status. An object keeps related values together under names, called properties. A property that holds a function is a method: something the object can do.

On the stage, rex is a box with three slots. The two tiles on the left are variables, and they are arrows, not boxes. A variable doesn't contain an object; it holds a reference to one, an address of where the object lives in memory. Copy the variable and you copy the arrow, not the object.

object.js

Inside a method, this is the object before the dot: in rex.speak(), it is rex. Press Step through and watch alias and rex change together.

That last line surprises everyone once. alias.name = "Max" renamed rex too, because there is one object with two arrows pointing at it. Most bugs with objects are really bugs about sharing, and this chapter comes back to that when it gets to immutability.

02 · CLASSES

Classes: a blueprint for objects

When you need many objects of the same shape, writing each one by hand gets old. A class is a blueprint: it says which properties every instance gets (in the constructor) and which methods they all share. new Dog("Rex", 3) builds an instance.

Watch the 3D. fido comes off the blueprint with its own name and age, but the speak() tile moves out of rex and onto the class. Methods aren't copied into each instance: there is one speak, kept on Dog.prototype, and every dog uses it. A thousand dogs, one function.

classes.js

Object.keys(rex) lists only name and age: the instance holds the data, the class holds the behaviour.

The code inside a class body always runs in strict mode, JavaScript's stricter dialect that turns several silent mistakes into errors. Classes arrived in ES2015; before that, people wrote the same thing with plain functions and prototypes, which is still what a class is underneath, as act 4 shows.

03 · ENCAPSULATION

Encapsulation: a shell round the data

The bigger a program, the more places could change a value by accident. Encapsulation means an object keeps its data private and lets the outside world in only through methods it chooses. The rules (no negative deposits, no overdrafts) then live in one place, and nothing can skip them.

In the 3D, the account gets a shell. #balance sits inside, locked. A call to deposit(50) comes in through a port on the front and updates the balance; a direct write from outside bounces off the shell. In JavaScript, a name that starts with # is truly private: writing acct.#balance outside the class is a SyntaxError, so the program won't even start.

account.js

Try adding acct.deposit(-5); before the try. Then try console.log(acct.#balance); and read the error.

Before # fields arrived in ES2022, JavaScript programmers hid data with a closure: a variable that only the functions created alongside it can see. You'll still meet this pattern everywhere:

closure-counter.js

Press Step through: count never appears as a property of c. It lives in makeCounter's scope, kept alive because the methods still refer to it.

04 · PROTOTYPES

Prototypes: how JavaScript really finds a property

Most languages with classes copy the class's layout into each object. JavaScript does something simpler and stranger: every object has a hidden link to another object, its prototype. When you read a property the object doesn't have, the engine follows the link and asks the prototype, then the prototype's prototype, until it finds the property or reaches null.

On the stage, a probe climbs that chain for five reads in turn. rex.name is found at once, on rex itself. rex.speak is one step up, on Dog.prototype. rex.eat is two up, rex.toString three, on Object.prototype, which nearly every object inherits from. rex.fly reaches null, and the answer is undefined, not an error.

prototypes.js

Object.create(p) makes an empty object whose prototype is p: the chain with no classes at all.

class is a friendlier syntax over exactly this. Dog extends Animal sets Dog.prototype's prototype to Animal.prototype, and a class is a function underneath. Here is the lookup the engine does on every property read, written out by hand:

lookup.js

The spec calls this algorithm OrdinaryGet. Real engines make it fast with hidden classes and inline caches, so a hot rex.speak() doesn't really climb every time.

1987the year Self was presented: a language with objects and prototypes but no classes. JavaScript's object model comes from it. Ungar and Smith, OOPSLA 1987

05 · COMPOSITION

Inheritance or composition

Inheritance says a thing is a kind of another thing: a Dog is an Animal, so it gets everything an Animal has. It is neat when the hierarchy is true, and painful when it isn't. Model birds, give Bird a fly(), and then meet a penguin:

the-penguin-problem.js

Code written for any Bird trusts fly(). Barbara Liskov put the rule precisely in 1987: a subtype must work everywhere its parent is expected. A penguin that throws breaks that promise.

Composition says a thing has abilities: build objects from small parts, and give each one only the parts it needs. On the stage the chain on the left is inheritance; on the right, a duck is assembled from canFly, canSwim and canQuack, docking one by one.

composition.js

Add a makeRobotDuck that can quack and fly but not swim. With inheritance, where would it go in the tree?

"Favor object composition over class inheritance" is one of the oldest pieces of advice in the field, from the 1994 book Design Patterns. It doesn't mean never inherit. Shallow hierarchies of things that truly are kinds of each other (an HttpError extends Error) are fine. Deep trees built to share code usually aren't.

06 · POLYMORPHISM

Interfaces and polymorphism

An interface is a promise about what an object can do, with nothing about how. "A shape has an area() method that returns a number." Polymorphism (Greek for "many forms") is the payoff: code written against the promise works with every object that keeps it, including ones invented later.

The socket on the stage is the interface. A circle, a square and a plain object take turns plugging in. The call is the same every time, s.area(); the answer is different, because each object brings its own method: 3.14, 4, 3.

shapes.js

The triangle isn't made from a class at all. JavaScript only asks "does it have an area method?" at the moment of the call. That's called duck typing: if it quacks like a duck…

JavaScript has no interface keyword; the promise lives in your head and your docs. TypeScript makes it something a tool can check:

shapes.ts · TypeScript
interface Shape {
  area(): number;
}
 
class Circle implements Shape {
  constructor(private r: number) {}
  area() { return Math.PI * this.r ** 2; }
}
 
// Structural typing: no "implements" needed, the shape of the value is enough
const shapes: Shape[] = [new Circle(1), { area: () => 4 }];
 
const bad: Shape = { size: 3 };
// error TS2353: Object literal may only specify known properties,
//               and 'size' does not exist in type 'Shape'.

Polymorphism is what replaces long chains of if (shape.type === "circle") … else if …. Every new kind of shape is a new class, not an edit to every function that handles shapes.

07 · STATIC VS DYNAMIC

Types: static or dynamic

Every value has a type: number, string, boolean, object, function. The type decides what you can do with it: you can multiply numbers, call functions, read .length of a string. The question is when a language checks that you used each value correctly.

The 3D shows types as shapes. A number is a square peg, a string is round, and add(a, b) has a square slot. On the top lane, JavaScript is dynamically typed: nothing is checked until the line runs, so the round peg reaches the slot, and add("2", 2) quietly returns "22". On the bottom lane, TypeScript is statically typed: a checker reads the code first, and its gate stops the round peg before anything runs.

dynamic.js

The last line throws a TypeError, but only when it is reached: delete it and the program is "fine". Dynamic checking finds a bug only on the paths you actually run.

A separate question is how strict a language is about mixing types. JavaScript converts freely ("2" + 2 is "22", "2" * 2 is 4). Python is dynamic too, but refuses to mix:

add.py · Python
def add(a, b):
    return a + b
 
add(2, 2)     # 4
add("2", 2)   # TypeError: can only concatenate str (not "int") to str
              # dynamic (checked while running) but strict about mixing

Static languages include Java, C, C#, Go, Rust and TypeScript; dynamic ones include JavaScript, Python, Ruby and Lisp. Static typing catches whole classes of mistakes early and makes editors smarter (autocomplete, safe renames). Dynamic typing is quicker to start with and more flexible. Large codebases have drifted towards static checking, often by adding it to a dynamic language: TypeScript for JavaScript, type hints and mypy for Python.

15%of real, fixed bugs in public JavaScript projects would have been caught by adding TypeScript or Flow annotations, in a study of 400 bugs. Gao, Bird and Barr, ICSE 2017

08 · TYPESCRIPT

TypeScript: types for JavaScript

TypeScript is JavaScript plus type annotations. You write a: number, the TypeScript compiler (tsc) checks every use, and then it erases the types and outputs plain JavaScript. Types exist only before the program runs; the browser never sees them. That is why this page's runner, which runs JavaScript, can't run TypeScript, and the examples here are shown with the real errors tsc 5.7 prints.

add.ts
function add(a: number, b: number): number {
  return a + b;
}
 
add(2, 2);     // fine
add("2", 2);
// error TS2345: Argument of type 'string' is not assignable
//               to parameter of type 'number'.
// ...reported in your editor, before the program ever runs.

You don't have to annotate everything. Type inference works types out from the values: let count = 3 is a number without you saying so, and an arrow function passed to map knows its parameter is a string. The usual habit is to annotate function parameters and let inference do the rest.

inference.ts
let count = 3;                  // inferred: number (no annotation needed)
count = "three";
// error TS2322: Type 'string' is not assignable to type 'number'.
 
const names = ["Ada", "Alan"];            // inferred: string[]
const lengths = names.map((n) => n.length); // inferred: number[]
// n is a string here: the editor offers .length, .toUpperCase(), ...
 
interface User {
  readonly id: number;
  name: string;
  email?: string;               // optional: string | undefined
}

How can a program know a type without running the code? It walks the parse tree from chapter 1 and works out each node's type from its children. Here is a tiny checker that does exactly that, which you can run:

type-checker.js

Our checker is stricter than TypeScript, which allows "a" + 1 (string plus number is a string). Add a rule for it.

Generics let one piece of code work for many types without forgetting which. On the stage the slot is first<T>: as each peg arrives, the slot reshapes to fit it, and TypeScript infers T from the argument. A square peg goes in and a square peg comes out, and the types say so.

generics.ts
// T is a type parameter: a placeholder filled in at each call
function first<T>(items: T[]): T | undefined {
  return items[0];
}
const n = first([3, 1, 2]);       // T = number,  n: number | undefined
const s = first(["a", "b"]);      // T = string,  s: string | undefined
 
// A constraint: T can be anything that HAS a length
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest("hello", "hi");           // T = string
longest([1, 2], [3]);             // T = number[]
longest(10, 20);
// error TS2345: Argument of type 'number' is not assignable
//               to parameter of type '{ length: number; }'.

One more tool takes TypeScript from helpful to powerful. A union type lists the shapes a value can have; checking a kind field narrows it to one; and the never trick makes the compiler tell you when you forgot a case:

union.ts
type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; side: number }
  | { kind: "triangle"; base: number; height: number };
 
function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.r ** 2;    // narrowed: here s is the circle
    case "square":
      return s.side ** 2;
    default:
      const unhandled: never = s;   // forgot a case? this line won't compile
      return unhandled;
  }
}
// error TS2322: Type '{ kind: "triangle"; base: number; height: number; }'
//               is not assignable to type 'never'.

09 · PURE & IMMUTABLE

Pure functions and immutability

Functional style starts with one rule: prefer pure functions. A pure function's output depends only on its inputs, and it changes nothing outside itself: no global variables, no editing its arguments, no printing. On the stage, addTax gives 125 every time. addToTotal gives 100, then 200, then 300 for the same input, because it reads and writes total, the box underneath it.

pure.js

Which of these two would you rather write a test for? A pure function needs no setup and no cleanup: input in, check the output.

The partner idea is immutability: never change a value once it exists; make a new one with the change. On the right of the stage, v2 and v3 are new boxes, each born from the one before. They all point at the same settings box, because it didn't change, so nothing was copied. That trick, structural sharing, is how immutable data stays cheap. Git stores your project history the same way: each commit is a new snapshot that reuses every file that didn't change.

immutable.js

Object.freeze is shallow: it froze v1, not the settings object inside it. The last two lines show the trap.

Real programs need side effects: saving files, drawing pixels, sending requests. The functional habit is to push them to the edges: a thin outer layer does the input and output, and a thick middle of pure functions does the thinking. Pure functions are also what makes code safe to run in parallel, on many cores or on a GPU: if nothing is shared and nothing changes, nothing can collide.

10 · MAP · FILTER · REDUCE

map, filter, reduce and composition

In JavaScript, functions are values: you can store them, pass them to other functions, and return them. A function that takes or returns functions is a higher-order function, and three of them do most of the work with lists. filter keeps the items that pass a test, map transforms each item, and reduce folds the whole list into one value.

The conveyor on the stage runs the program below. The numbers 1 to 6 enter; filter drops the odd ones through the floor; map squares what is left, 2, 4 and 6 into 4, 16 and 36; and reduce adds them into one box: 56.

pipeline.js

Change the filter to n > 3, or the map to n * 10, and predict the answer before you run it.

Compare the same job as a loop. Both are fine; the loop says how, step by step, while the pipeline says what, and each step can be read, tested and reused on its own.

loop.js

There is no magic in them. Each is a few lines you could write yourself, and writing them once is the best way to understand them:

my-map-filter-reduce.js

Press Step through and watch acc grow inside reduce: 0, 4, 20, 56.

Composition is the functional answer to building big things: join small functions into a bigger one, where the output of each is the input of the next. pipe is a one-line higher-order function that does it:

compose.js

11 · WHICH, WHEN

Which style, when

The stage pulls back to show all six stations. The left half is object-oriented, the middle is types, the right half is functional. They answer different questions, so real programs mix them.

three-styles.js

Three styles, one answer. Which would you want to change if "paid" also had to exclude refunds?

  • Reach for objects when something has state that changes over time and rules that must hold: a bank account, a game character, a connection to a database, a UI widget. Encapsulation keeps the rules in one place.
  • Reach for functional style when data flows in, gets transformed, and flows out: reports, search results, prices, anything you can describe as a pipeline. Pure functions are the easiest code there is to test and to run in parallel.
  • Reach for static types as soon as a program will live longer than a week or be read by more than one person. They are documentation the compiler keeps honest.
  • Plain imperative loops are fine for short, hot or genuinely step-by-step code. Clarity beats purity.

One more style deserves a name: declarative code says what result you want and lets something else decide how to compute it. SQL is the famous example, and the same pipeline from the conveyor looks like this in SQL and in Python:

pipeline.sql · SQL
-- Declarative: say WHAT you want; the database decides how.
SELECT SUM(n * n)
FROM nums
WHERE n % 2 = 0;      -- 56, the same pipeline as filter, map, reduce
pipeline.py · Python
nums = [1, 2, 3, 4, 5, 6]
sum(n * n for n in nums if n % 2 == 0)   # 56: Python's comprehension

A common modern shape is functional core, imperative shell: plain data described by types, transformed by pure functions, with a few objects at the edges owning the things that really change: the network, the screen, the database.

PRACTICE

Now you do it

Type the code yourself rather than pasting it: producing an answer builds memory that reading does not. Use the hints one at a time.

1

Your first class

Write a class Counter. A new counter has a count property of 0. increment() adds 1 and returns the new count; reset() sets it back to 0.

exercise-1.js
2

Guard the data

Make Temperature keep its value in a private #celsius field. Add a fahrenheit getter, and a celsius setter that throws a RangeError for anything below absolute zero (−273.15). Nothing should show up in Object.keys.

exercise-2.js
3

Plug in a new shape

totalArea works on anything with an area() method. Add a Rectangle class (width and height) so the last line prints 10, without changing totalArea.

exercise-3.js
4

Walk the prototype chain

Write findOwner(obj, key): return the object in obj's prototype chain that has key as its own property, or null if none does.

exercise-4.js
5

Make it pure

addItem changes the cart it is given, so the "before" cart is lost. Rewrite it to return a new cart and leave the old one alone.

exercise-5.js
6

A pipeline, no loops

Using filter, map and reduce (no for), set revenue to the total amount of the orders that are paid.

exercise-6.js
7

Build pipe yourself

Write pipe(...fns): it returns a function that passes its input through each function in order. pipe() with no functions should return the input unchanged.

exercise-7.js

RECALL

Check yourself, from memory

Answer each one before you open it. Then come back tomorrow, and again next week, and answer them cold: retrieval spaced over days is the most reliable study method we know of. Why that works.

In rex.speak(), what is this, and why?

Why is rex.speak === fido.speak true for two instances of a class?

What happens when you read a property an object doesn't have?

Give one reason to prefer composition to inheritance.

Static or dynamic: when does each find a type error?

What makes a function pure, and why care?

What is the T in first<T>(items: T[]): T for?

GO FURTHER

Resources

SOURCES

Where the facts come from

  1. MDN: Working with objects Properties, methods and this; objects are assigned by reference.
  2. MDN: Classes Class bodies run in strict mode; methods are defined on the prototype.
  3. MDN: Private properties #fields, added in ES2022; reading one from outside the class is a SyntaxError.
  4. MDN: Inheritance and the prototype chain
  5. ECMAScript 2025 spec: OrdinaryGet The property lookup algorithm: own property, else ask [[GetPrototypeOf]], until null.
  6. MDN: Object.create
  7. MDN: Object.hasOwn
  8. Ungar and Smith, Self: The Power of Simplicity (OOPSLA 1987) The prototype-based language that JavaScript's objects come from.
  9. Wirfs-Brock and Eich, JavaScript: The First 20 Years (HOPL 2020) JavaScript's prototypes came from Self and its first-class functions from Scheme.
  10. Nygaard and Dahl, The Development of the SIMULA Languages (HOPL 1978) Simula 67 introduced classes, objects, subclasses and virtual procedures.
  11. Alan Kay on the meaning of "object-oriented programming" (2003) "OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things."
  12. Liskov, Data Abstraction and Hierarchy (OOPSLA 1987 keynote) The substitution principle: a subtype must work wherever its parent is expected.
  13. Gamma, Helm, Johnson, Vlissides, Design Patterns (1994) "Favor object composition over class inheritance."
  14. MDN: typeof Including why typeof null is "object".
  15. Gao, Bird and Barr, To Type or Not to Type (ICSE 2017) Flow or TypeScript would have caught about 15% of the public bugs studied.
  16. TypeScript Handbook: Type Inference
  17. TypeScript Handbook: Generics
  18. TypeScript Handbook: Narrowing Discriminated unions and exhaustiveness checking with never.
  19. TypeScript Handbook: Type Compatibility TypeScript's type system is structural.
  20. MDN: Object.freeze Shallow; assignments to a frozen object fail silently, or throw in strict mode.
  21. MDN: Array.prototype.toSorted The copying sort, added in ES2023.
  22. MDN: Array.prototype.reduce
  23. McCarthy, Recursive Functions of Symbolic Expressions (CACM 1960) Lisp: functions as values, and the maplist function.
  24. Dean and Ghemawat, MapReduce (OSDI 2004) map and reduce, borrowed from functional languages, spread over thousands of machines.