Before you begin
Why this matters
Every file you write is used by other files. Knowing which names they can reach lets you change the rest freely, keep a class’s rules in one place, and spot the method that quietly hands its data out.
What this helps you reason about
- Predict which names outside code can use, and read the compiler’s error when it uses another.
- Explain how private fields and checking methods keep a rule true for every object.
- Spot a method that returns a private array, and fix it with a copy.
An office runs a fire-safety course with a few places. Staff sign up until the course is full. The program that runs the sign-ups has a rule: never more names than places. This lesson shows how a module and a class decide which names other code may use, how that keeps the rule true, and how a method can still give the protected data away.
Already know this? The short version
- Only declarations marked export can be used outside their file or namespace; anything else is private to it, and an outside use is a compile error.
- private on a class field limits it to the class’s own code; it is compile-time only, while #name fields are private at run time too.
- Private fields plus methods that check a rule keep that rule true for every object (encapsulation); a method that returns the private array hands out the array, so return a copy or a readonly type.
- The file is the module: a file’s top-level names are private unless exported.
Words in this lesson (6)
- namespace
- A named block, namespace Steps { … }, that groups declarations. Code outside it uses a declaration marked export as Steps.goal.
- export
- A keyword before a declaration, as in export function placesLeft, that lets code outside its module or namespace use it. A declaration without export is private to the module.
- private
- A TypeScript marker on a class field or method, as in private names, that makes the compiler reject every use from outside the class. It is checked before the program runs and has no effect while it runs.
- invariant
- A rule that every value of a type must follow, such as “weekly hours are from 1 to 40”.
- encapsulation
- Keeping a type’s data private and letting other code change it only through functions that keep the type’s rules.
- alias
- A second name for data that another name already reaches. A change made through one name is seen through the other.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Exported names
Which names can other code use?
A module decides which of its names other code may use. Modules, packages, and namespaces showed how code names something in another module; this lesson is about which names it is allowed to use.
A name that other code may use is exported. Any other name is private to the module: code inside the module can use it, and code outside cannot. In TypeScript, export before a declaration exports it. In the namespace below, placesLeft is exported and places is not. Outside the namespace, Course.placesLeft works, and Course.places is a compile error: Property 'places' does not exist on type 'typeof Course'.
A private name is free to change. No other code can use it, so renaming or removing it breaks nothing outside the module.
namespace Course {
const places = 3; // private: no export
export function placesLeft(taken: number): number {
return places - taken;
}
}
console.log(Course.placesLeft(1)); // 2
// Course.places is a compile error Only exported names can be used outside a module; every other name is private to it.
Coming from Python: A leading underscore in Python is a convention; in TypeScript, a name without export cannot be imported at all.
Precisely: private at run time too
The compiled namespace is a function that runs once and fills an object. places is a local variable of that function, so it is private while the program runs too: the object Course has no places property at all.
Experiment
Call an exported function that uses a private constant
Only declarations marked export become part of the namespace. places is declared inside the block without export, so code inside the block can use it, and code outside cannot name it.
Why it matters Leave export off every name that other code does not need. The compiler then rejects any outside use, and you can change that name later without breaking other code.
Predict both printed numbers. Then change the last line to console.log(Course.places); and predict again.
namespace Course {
const places = 3;
export function placesLeft(taken: number): number {
return places - taken;
}
}
console.log(Course.placesLeft(1));
console.log(Course.placesLeft(3));
What to look for
After the namespace block runs, Course shows {placesLeft: function placesLeft(taken)}: the object has no places property. Step into placesLeft: the frame lists taken = 1 and places = 3, because the function can still use the constant around it. The stepper sometimes stops on the same line twice; that is normal for this debugger. The run prints 2, then 0. After the edit, nothing runs, and the compiler reports Line 10: Property 'places' does not exist on type 'typeof Course'.
Expected output
2
003 / Leaks
How does hidden data still get changed?
A private field protects the field, not the data it refers to. When a method returns this.names, it returns a reference to the same array. The caller now holds an alias, a second name for that array, and can push to it without signUp.
Hand out something that cannot change the private data instead. this.names.slice() returns a copy, so the caller’s changes stay in the copy. A return type of readonly string[] makes the compiler refuse push and other changes; it costs nothing, but like private it is checked by the compiler only.
list(): string[] {
return this.names.slice(); // a copy: changes stay with the caller
}
list(): readonly string[] {
return this.names; // the compiler refuses push on the result
} A method that returns a private array hands out the array; return a copy or a readonly type.
Precisely: objects inside a copy
slice() copies the array, not the objects in it. If the array holds objects that the class relies on, a caller can still change those objects through the copy. Shallow and deep copying shows the difference.
Experiment
Change a private array through a method
list() returned the array that the private field refers to, not a copy. lines became an alias, a second name for that array, so the push changed the course. private stops code outside from writing course.names; it does not stop a method from handing the array out.
Why it matters A method that returns a private array or object hands out the field’s data. Return a copy when callers may change what they get.
Predict both printed lines. Then change return this.names; to return this.names.slice(); and predict again.
class Course {
private names: string[];
constructor(first: string) {
this.names = [first];
}
list(): string[] {
return this.names;
}
count(): number {
return this.names.length;
}
}
const course = new Course("Ana");
const lines = course.list();
lines.push("2 places left");
console.log(lines);
console.log(course.count());
What to look for
After const lines = course.list(); runs, lines shows ["Ana"]. At the next stop, after the push, both lines and course show the new element: course is {names: ["Ana", "2 places left"]}. The run prints [ 'Ana', '2 places left' ], then 2. After the edit, course keeps {names: ["Ana"]} while lines grows, and the second line is 1.
Expected output
[ 'Ana', '2 places left' ]
204 / Boundaries
Where is the boundary in a real program?
In a real TypeScript program, the boundary is the file. A file’s top-level names are private unless marked export, and other files can import only the exported ones. Inside the file, every function and class can use every other.
So keep each file small, and export only what other files need. Inside a file, private and # fields draw a smaller boundary around one class.
// course.ts
const places = 3; // private to this file
export function placesLeft(taken: number): number {
return places - taken;
}
// desk.ts
import { placesLeft } from "./course";
console.log(placesLeft(1)); The file is the module: export what other files need, and nothing else.
Precisely: what a package exports
A library can also list its entry files in the exports field of its package.json; Node.js then refuses to load any other file of that library from outside it. Using modules that other people wrote is a later topic in this section: the standard library and third-party packages.
05 / Try it
Can you explain a fresh example?
Decide, for each change, whether it reaches the private data or only a copy. A copy can be made on the way out of a class, and data can also come in.
class Wardens {
private names: string[];
constructor(names: string[]) {
this.names = names;
}
list(): string[] {
return this.names.slice();
}
first(): string {
return this.names[0];
}
}
const start = ["Ida", "Max"];
const wardens = new Wardens(start);
wardens.list().push("Lou");
start[0] = "Kim";
console.log(wardens.first(), wardens.list().length);
What is printed?
Reveal the reasoning
Kim 2. list() returns a copy, so push adds Lou to the copy only, and the list still has 2 names. But the constructor stored the array it was given, so start and the private field are one array, and start[0] = "Kim" changes the wardens. To protect the data, copy it on the way in as well: this.names = names.slice().
Practice tickets
- EXPV-101 · The pool car keys go to someone who signed up later → Free
- EXPV-102 · The reception screen shows more Wi-Fi vouchers than the box can hold → Sign in · Pro
- EXPV-103 · A corrected day does not change the week’s hours → Sign in · Pro
Keep exploring
The standard library and third-party packages →