← Fundamentals
Guided lesson 5 questions · 6 languages

Exports, visibility, and encapsulation

Which names other code may use, and how hiding data keeps a value valid.

A module decides which of its names other code may use; using any other name is a compile error. Hiding a type’s fields, and changing them only through functions that check its rules, keeps those rules true, as long as no function hands the hidden data back out.

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.

Go to the final check· Go to the practice tickets

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.

main.ts
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
0

02 / Hidden fields

Why keep a field private?

A class can hide its fields too. private before a field lets only code inside the class use it; other code must call the class’s methods.

Constructors and creating values checked a type’s rule, its invariant, in the one function that creates values. A rule must also stay true through every later change. When the field is private, the class’s own methods are the only code that can change it, so checking those methods is enough. Keeping a type’s data private, and changing it only through functions that keep its rules, is called encapsulation.

Below, a course never holds more names than places, because signUp is the only way to add a name. Outside the class, course.names.push("Cy") is a compile error: Property 'names' is private and only accessible within class 'Course'.

main.ts
class Course {
  private names: string[] = [];
  private places: number;

  constructor(places: number) {
    this.places = places;
  }

  signUp(name: string): boolean {
    if (this.names.length === this.places) return false;
    this.names.push(name);
    return true;
  }
}

Private fields, changed only by methods that check the rule, keep the rule true for every object.

Coming from Java: A TypeScript class member with no marker is public, not package-private; write private for each field you want to hide.

Precisely: private and #

private is removed when TypeScript is compiled to JavaScript. While the program runs, names is an ordinary property, and (course as any).names reaches it; that is also why the stepper shows it. A field written #names is private in JavaScript itself, so no code outside the class can reach it, even at run time.

Experiment

Try to add a name past the limit

private names lets only code inside the class use the field. signUp is the only way to add a name, and it refuses when the list is full, so no course can hold more names than places.

Why it matters Make a field private when the class has a rule about it. Then the class’s own methods are the only code that can break the rule, and you check them alone.

Predict both printed lines. Then add the line course.names.push("Cy"); just before the last line, and predict again.

class Course {
  private names: string[];
  private places: number;

  constructor(places: number) {
    this.names = [];
    this.places = places;
  }

  signUp(name: string): boolean {
    if (this.names.length === this.places) {
      return false;
    }
    this.names.push(name);
    return true;
  }
}

const course = new Course(2);
console.log(course.signUp("Ana"), course.signUp("Ben"));
console.log(course.signUp("Cy"));
What to look for

The first stop inside new is in a frame named <instance_members_initializer>, where Node prepares the object’s fields; the constructor’s frame then lists this first, as {names: undefined, places: undefined}, and places = 2. this fills in line by line until it shows {names: [], places: 2}. Back at the top level, course shows {names: [], places: 2}: the debugger shows private fields, because private is checked by the compiler, not hidden. Each signUp call adds one name until course shows {names: ["Ana", "Ben"], places: 2}; the third call stops on return false;. The run prints true true, then false. After the edit, nothing runs, and the compiler reports Line 21: Property 'names' is private and only accessible within class 'Course'.

Expected output

true true
false

03 / 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.

main.ts
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' ]
2

04 / 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.

main.ts
// 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.

main.ts
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

  1. EXPV-101 · The pool car keys go to someone who signed up later → Free
  2. EXPV-102 · The reception screen shows more Wi-Fi vouchers than the box can hold → Sign in · Pro
  3. EXPV-103 · A corrected day does not change the week’s hours → Sign in · Pro

Keep exploring

The standard library and third-party packages →

Primary references