Base CS from zero
Modules
A single object bundles one piece of state with its operations. But a real program has dozens of objects, hundreds of functions, and many helper values. If they all sat in one flat space, every name would be visible to every other name. Any function could read or overwrite any value. A typo in one corner could quietly break code in a far corner.
A program that size needs a coarser unit than the object — a way to draw a line around a group of related code and say: inside this line is one thing; outside, you may use only what I choose to show you.
That line, and the code it encloses, is a module. This lesson defines what a module is, what its boundary does, and how it decides which of its names the rest of the program is allowed to see.
After this lesson you can define a module as a group of related code behind a boundary, explain what an export is and how it forms the module’s public interface, describe what the boundary prevents, and explain why grouping names under a module keeps them from colliding.
A module: related code behind a boundary. A module is a unit of code that groups related definitions — functions, objects, values — and draws a boundary around them.
The boundary is not a physical thing. It is a rule the language enforces: code on one side of the boundary cannot freely see the names on the other side. By default, every name defined inside a module is private to that module — visible only to other code inside the same module.
This extends an idea you already know. In Unit 08 lesson 04 you saw scope: a variable declared inside a function is visible only inside that function. A module applies the same principle one level up — a name defined inside a module is, by default, visible only inside that module. The function gave a boundary around a few variables; the module gives a boundary around a whole group of definitions.
The export: the module’s public interface. If everything in a module were private, no other code could ever use it. A module would be sealed shut and useless.
So a module can mark some of its names as exports. An export is a name the module explicitly chooses to make visible to code outside its boundary. Everything not marked as an export stays private.
The set of exported names is the module’s public interface — the same idea as the interface from lesson 01, now at the scale of a whole module. Outside code may use the exported names and nothing else. The private names — helper functions, internal values, half-finished pieces — are the module’s hidden implementation. A module is therefore an abstraction: a boundary with a chosen interface on the outside and a hidden implementation within.
What the boundary prevents. The boundary does real protective work. Because outside code can reach a module only through its exports, two failures become impossible.
First, outside code cannot touch private state. A module’s internal value cannot be read or overwritten from outside. Whatever invariant the module maintains — a count that must stay non-negative, a cache that must stay consistent — is safe, because the only code that can change it is the module’s own code.
Second, a change to a private name cannot break outside code. If a name is private, nothing outside depends on it, so the module’s author can rename it, delete it, or rewrite it freely. Only the exported names are a commitment. This is the lesson 01 rule again — fixed interface, replaceable implementation — but the boundary is what enforces it: the language makes it impossible for outside code to accidentally depend on a private detail.
Namespacing: why names stop colliding. A program may need two functions both sensibly
called format — one formats dates, one formats currency. In a single flat space, the two
definitions would collide: the second would overwrite the first.
A module gives each name a home. The date module’s format and the currency module’s
format are different names because each is reached through its own module:
dateModule.format and currencyModule.format. Outside code reaches a module’s exports
through the module’s name — a named handle — using the same dot syntax you used for an
object’s fields in Unit 09. The module name in front keeps them
apart. This separation — names grouped under a module so identical short names do not
clash — is called namespacing.
Namespacing is why a large program does not run out of good names. Each module is a fresh space; a name used inside one module says nothing about a name inside another. Without it, every name in a program would compete with every other name, and large programs would be unwritable.
Why this works
Why enforce the boundary instead of just trusting programmers? A team could agree by convention “do not touch another module’s internals” — but conventions are forgotten, and under deadline pressure someone always reaches across. An enforced boundary removes the question entirely: the private name is simply not visible, so no one can depend on it, by accident or on purpose. The module’s author is then free to change every private detail, knowing for certain that nothing outside relies on it. The boundary turns a hope into a guarantee.
Reading a small module and finding its interface.
Here is a module with four definitions:
// module: text-format
const TABLE = { ... }; // a lookup table
function pad(s: string): string { ... } // helper
function trim(s: string): string { ... } // helper
export function format(s: string): string {
return pad(trim(s)); // uses both helpers
}Which names are public? Scan for the keyword export. Only format has it. So the
module’s public interface is exactly one name: format.
Which names are private? Everything without export: TABLE, pad, and trim —
three private names. Outside code cannot name any of them.
Can outside code call pad? No. pad is private. Code in another module that wrote
textFormat.pad("x") would fail — the name is not exported, so it is not visible across
the boundary.
Can the author rename TABLE to LOOKUP? Yes, safely. TABLE is private; nothing
outside the module can refer to it, so renaming it cannot break any other code. Only
format is a commitment to the outside world.
The takeaway: the module exposed one carefully chosen name and hid three. A user of
the module thinks about format and nothing else — exactly the reduction abstraction is
for, now applied to a whole group of code.
Common mistake
A common mistake is thinking a private name does not run. It runs perfectly well. In the
example, pad and trim execute every time format is called — they do real work.
“Private” does not mean inactive; it means not nameable from outside the module. The
distinction is visibility, not whether the code executes.
A module defines 6 names. It marks 2 of them as exports. How many of its names are private?
The text-format module exports only format. How many of its names can outside code call directly?
A module's helper function is private. The module's author renames that helper. How many lines of outside code must change because of the rename?
Module A has a function named format. Module B also has a function named format. Reached as A.format and B.format, do these two names collide? Type 1 for yes, 0 for no.
A module keeps a private counter that must never go below 0. How many places in the whole program can change that counter's value?
What is a module, and what does its boundary do?
A module is a unit of code that groups related definitions behind a boundary. By
default every name inside a module is private — visible only to other code in the same
module, the same way a variable is scoped to its function. A module marks chosen names as
exports; the set of exported names is its public interface, and everything else is
hidden implementation. The boundary does real work: outside code cannot read or overwrite
a module’s private state, and a change to a private name cannot break any code outside, so
the author can rewrite internals freely. Grouping names under a module also gives each one
a home — namespacing — so two modules can both use a short name like format without
colliding. A module is abstraction applied to a whole group of code: a chosen interface
out front, a protected implementation within.