cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

public, private, protected & #private

Control who can use a class's fields with public, private, and protected, stop changes with readonly, and see how private differs from JavaScript's #private.

By the end, you can
  • 01
    Choose who can use a memberMark fields and methods public, protected, or private, and predict which lines TypeScript marks.
  • 02
    Stop changes with readonlyMake a field that is set once in the constructor, and know what readonly doesn't stop.
  • 03
    Pick private or #privateExplain why TypeScript's private is gone when the code runs, and when JavaScript's #private is the better choice.

Who can use a class member

The Classes in TypeScript lesson ended with a quantity field for a shop page. Its setter keeps the quantity at least 1. But the number it guards, count, is a field any code can change:

A setter that outside code can skipPop out in the code editor (opens in a new tab)TypeScript
class QuantityField {  count = 1;   get quantity() {    return this.count;  }   set quantity(value: number) {    this.count = Math.max(1, Math.round(value));  }} const field = new QuantityField();field.quantity = -4;console.log(field.quantity);field.count = -4;console.log(field.quantity);

Line 2 is the field, count. Lines 8 to 10 are the setter: it rounds the new value and keeps it at least 1. Line 14 sets the quantity to -4 through the setter, so line 15 prints 1. Line 16 skips the setter and writes -4 straight into count, and line 17 prints -4. A shopper could order minus four cakes. TypeScript sees nothing wrong.

A field or method of a class is called a member. An access modifier is a word in front of a member that says which code may use it. Put private in front of count:

count made private (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class QuantityField {  private count = 1;   get quantity() {    return this.count;  }   set quantity(value: number) {    this.count = Math.max(1, Math.round(value));  }} const field = new QuantityField();field.quantity = -4;console.log(field.quantity);field.count = -4;console.log(field.quantity);

The only change is line 2: private count = 1. The getter and setter are inside the class, so they still use count. Line 16 is outside the class, and TypeScript marks it, on purpose here: Property 'count' is private and only accessible within class 'QuantityField'. In plain words: only code written inside QuantityField may use count. Now the only way in from outside is the setter.

A window titled QuantityField, the class, holds a card private count = 1. Inside it, set quantity(value) points at the card: allowed. Outside the window, a card field.count = -4 points at it: error.QuantityFieldprivate count = 1set quantity(value)allowedoutside the classfield.count = -4error
With private count, the setter inside the class can change count, and it keeps the value at least 1. Line 16, outside the class, gets an error.
Real-life analogySigns in a shop

A shop puts signs on its doors. Customers walk the shop floor. The back room says "Staff only", and new staff in training may go in too. The office says "Manager only". Everyone reads the signs and keeps to them.

In real life: The shop floor, where any customer may walk
In TypeScript: public members, which any code may use
In real life: The back room, for staff and staff in training
In TypeScript: protected members, for the class and its subclasses
In real life: The manager's office, for the manager only
In TypeScript: private members, for the class itself
In real life: A price tag printed once and never changed
In TypeScript: A readonly field, set once in the constructor
The main idea

Access modifiers say which code may use a class member. public (the default) means any code. protected means the class and its subclasses. private means only the class itself. TypeScript checks them before the code runs; JavaScript's own #private fields are also guarded while it runs.

public and private

Every member is public unless you say otherwise, so writing public changes nothing; some teams write it anyway to be clear. private works on methods as well as fields. Here is a cart that keeps its list and a helper to itself:

A cart with a private list and a private method (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class Cart {  private items: string[] = [];   public add(name: string) {    this.items.push(name);    this.log(`Added ${name}`);  }   private log(message: string) {    console.log(message);  }} const cart = new Cart();cart.add("Tea");cart.items.push("Free cake");cart.log("Hello");

Line 2 makes items private. Line 4 makes add public, written out here to show the word. Lines 5 and 6 are inside the class, so add may use the private items and the private log method on line 9. Line 15 calls add, which prints Added Tea. Lines 16 and 17 are outside the class, so both get Property '…' is private and only accessible within class 'Cart', on purpose here. Outside code has one way to add an item: add.

The errors don't stop the code. Run it anyway and line 16 really pushes Free cake, and line 17 prints Hello: private is a TypeScript check, like the other types. The last section of this lesson shows what that means while the page runs.

protected and subclasses

A subclass is a class made from another with extends: it gets every member of its base class and can add more (the Class inheritance lesson covers extends). private keeps subclasses out too. When a subclass needs a member that outside code shouldn't touch, use protected:

A sale price built on a protected price (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class Product {  constructor(protected basePrice: number) {}} class SaleProduct extends Product {  get price() {    return this.basePrice - 1;  }} const cake = new SaleProduct(4);console.log(cake.price);console.log(cake.basePrice);

Line 2 is a parameter property with protected: Product gets a basePrice field that only Product and its subclasses may use. Line 5 makes SaleProduct a subclass. Line 7, inside its getter, reads this.basePrice, which is allowed, and line 12 prints 3. Line 13 is outside both classes, so TypeScript marks it, on purpose here: Property 'basePrice' is protected and only accessible within class 'Product' and its subclasses.

Now compare all three on one field. Each button puts a different modifier in front of price on line 2. Line 7 uses it from the subclass, and line 13 from outside:

Playground: who can use price
A product and a sale productPop out in the code editor (opens in a new tab)TypeScript
class Product {  public price = 4;} class SaleProduct extends Product {  salePrice() {    return this.price - 1;  }} const cake = new SaleProduct();console.log(cake.salePrice());console.log(cake.price);
Console output3, 4
What happens
Line 2public price = 4;
TypeScriptNo errors
Try it yourself

public lets any code use price: the Product class, the SaleProduct subclass on line 7, and the code outside on line 13. No errors. It prints 3, then 4.

TypeScript checks the code for real each time. Each button changes only line 2. Reset goes back to public.

With public or No modifier, there are no errors. protected marks only line 13. private marks lines 7 and 13. The console always shows 3 and 4: the modifiers change what TypeScript allows, not what the code does. You can also press Edit & run and try a modifier on the salePrice method on line 6.

Three nested boxes. The outer box, all code, has the word public. Inside it, the box subclasses has the word protected. Inside that, the box Product has the word private.all codesubclassesProductprivateprotectedpublic
Each modifier says how far a member reaches. private stays in its class, Product. protected also reaches subclasses like SaleProduct. public reaches all code.

The table below sums it up. Read each row as: this code wants to use a member, and may it, for each modifier.

Who may use a member, for each modifier
Who wants to use the member?publicprotectedprivate
Code inside the class itselfYes.Yes.Yes.
Code inside a subclassYes.Yes.No.
Code outside the classesYes.No.No.

readonly fields

Some values should never change once an object exists, like an order's id. readonly in front of a field means it gets its value where it's declared or in the constructor, and nowhere else:

An order id that can't change (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class Order {  readonly id: string;   constructor(id: string) {    this.id = id;  }   rename(newId: string) {    this.id = newId;  }} const order = new Order("A-7");order.id = "B-2";console.log(order.id);

Line 2 declares readonly id. Line 5, in the constructor, gives it its value, which is allowed. Line 9, in a method, and line 14, outside the class, both try to change it, and TypeScript marks them, on purpose here: Cannot assign to 'id' because it is a read-only property. "Assign" means "give a new value". readonly is about changing, not reading: any code may still read order.id.

Left, a window titled constructor holds this.id = id, marked allowed. An arrow leads right to a window titled later, which holds order.id = B-2, marked error.constructorthis.id = idallowedline 5laterorder.id = "B-2"errorlines 9 and 14
A readonly field gets its value in the constructor, like line 5. After that, any change is an error, like lines 9 and 14.

Run it anyway, and line 15 prints B-2. Like private, readonly is checked by TypeScript and removed before the code runs.

Modifiers work in parameter properties too, and you can put two together: private readonly is a field only the class can read and nobody can change.

readonly and private readonly in the constructorPop out in the code editor (opens in a new tab)TypeScript
class Order {  constructor(    readonly id: string,    private readonly placedOn: string,  ) {}   summary() {    return `${this.id}, placed ${this.placedOn}`;  }} const order = new Order("A-7", "May 3");console.log(order.summary());console.log(order.id);//                ^?

Line 3 makes a public id that can't change. Line 4 makes placedOn, which only the class can read and nobody can change. Line 8 reads both from inside the class, and line 13 prints A-7, placed May 3. Line 14 reads order.id from outside, which is fine, and prints A-7. The query on line 15 shows (property) Order.id: string.

private vs #private at runtime

"At runtime" means while the code runs. TypeScript removes private along with the other types, so a private field becomes an ordinary property. TypeScript even lets you read one with square brackets, on purpose, so that things like tests can reach it:

A private token, read with square bracketsPop out in the code editor (opens in a new tab)TypeScript
class Account {  private token = "abc123";} const account = new Account();console.log(account["token"]);console.log(JSON.stringify(account));

Line 2 makes token private. Line 6 reads it as account["token"], with no error, and prints abc123. Line 7 turns the object into text with JSON.stringify and prints {"token":"abc123"}. Here is what the browser gets:

What the browser gets: no private leftPop out in the code editor (opens in a new tab)JavaScript
class Account {
  token = "abc123";
}

const account = new Account();
console.log(account["token"]);
console.log(JSON.stringify(account));

Line 2 is a plain field, token. Nothing in the JavaScript says it was private, so any script on the page can read it.

JavaScript has its own private fields, written with # in front of the name (the Private members lesson covers them). They stay private while the code runs:

A #private tokenPop out in the code editor (opens in a new tab)TypeScript
class Account {  #token = "abc123";   hasToken() {    return this.#token.length > 0;  }} const account = new Account();console.log(account.hasToken());console.log(JSON.stringify(account));

Line 2 declares #token. The # is part of the name. The method on lines 4 to 6 uses this.#token from inside the class, and line 10 prints true. Line 11 prints {}: #token isn't an ordinary property, so JSON.stringify doesn't see it.

#token read from outside (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class Account {  #token = "abc123";} const account = new Account();console.log(account.#token);

Line 6 reads account.#token from outside the class. TypeScript marks it, on purpose here: Property '#token' is not accessible outside class 'Account' because it has a private identifier. An "identifier" is a name, and a name starting with # is a private one. JavaScript agrees: the code doesn't even start, and stops with a SyntaxError.

Left, private token: a console prints the token and its value abc123. Right, #token: a console prints an empty object, so the token stays hidden.private tokenconsole{"token":"abc123"}the token shows#tokenconsole{}the token stays hidden
The same JSON.stringify(account) while the code runs. A private field is an ordinary property by then, so it shows. A #private field stays hidden.
Real-life analogyA sign on a door, or a lock

A sign on the office door keeps out everyone who reads it, and that's most people. But the door itself isn't locked. A locked door keeps everyone out, whether they read signs or not.

In real life: A "Manager only" sign on the office door
In TypeScript: TypeScript's private
In real life: Staff read the sign and stay out
In TypeScript: TypeScript marks code that uses the member
In real life: The door itself opens for anyone
In TypeScript: While the code runs, the field is an ordinary property
In real life: A locked door that only the manager's key opens
In TypeScript: JavaScript's #private, guarded while the code runs

The table below compares the two. Read each row as a question, then the answer for each kind of private field.

private and #private, side by side
Questionprivate token#token
Who checks it?TypeScript, before the code runs.JavaScript, while the code runs. TypeScript checks it too.
Can outside code read it while the page runs?Yes. account["token"] gives abc123.No. account.#token outside the class is a SyntaxError.
Does JSON.stringify show it?Yes: {"token":"abc123"}.No: {}.
Can a subclass use it?No. Use protected for that.No. Only code inside the class that declares it.
Does it work in plain JavaScript?No. It's TypeScript syntax, removed before the code runs.Yes. It's part of JavaScript.

Which should you use? For keeping your own code tidy, private is enough, and it works with parameter properties like constructor(private name: string). When the value must stay hidden while the page runs, use #private. Neither one is a place for real secrets: anything in the browser can be read by someone using the browser.

A cart nobody can break from outside

Here is a shop's cart that uses all of it. ShoppingCart keeps its items protected, so its subclasses can read them but outside code has to use add, which refuses a price of 0 or less. The owner is readonly. GiftCart adds 2 for wrapping each item, kept in a #wrapping field. Its total getter uses super.total on line 21, the base class's total, and adds the wrapping. Each button tries a different line 28:

Playground: try to break the cart
type CartItem = { name: string; price: number }; class ShoppingCart {  protected items: CartItem[] = [];   constructor(readonly owner: string) {}   add(item: CartItem) {    if (item.price > 0) this.items.push(item);  }   get total() {    return this.items.reduce((sum, item) => sum + item.price, 0);  }} class GiftCart extends ShoppingCart {  #wrapping = 2;   get total() {    return super.total + this.#wrapping * this.items.length;  }} const cart = new GiftCart("Asha");cart.add({ name: "Tea", price: 3 });cart.add({ name: "Cake", price: 4 });cart.add({ name: "Free cake", price: -4 });console.log(`${cart.owner} pays $${cart.total}`);
When it runsAsha pays $11
What happens
Line 28cart.add({ name: "Free cake", price: -4 });
TypeScriptNo errors
Try it yourself

add is the way in, and line 9 refuses a price of 0 or less. No errors, and the free cake never gets in: Tea 3 and Cake 4, plus 2 for wrapping each one, prints Asha pays $11.

TypeScript checks the code for real each time, and the output is what the code does when it runs. Each button changes only line 28. Reset goes back to Use add.

Use add is the way the class allows: no errors, and the bad price is refused. Each of the other three skips a rule, and TypeScript marks line 28. When the code runs, Push to items and Rename the owner still get through, because protected and readonly are gone by then. Change the wrapping doesn't: JavaScript itself guards #wrapping.

Error or fine?
  • class Cart { private items: string[] = [] } new Cart().items;
  • class Cart { private items: string[] = []; count() { return this.items.length; } }
  • class Cart { protected items: string[] = [] } class Wishlist extends Cart { first() { return this.items[0]; } }
  • class Cart { protected items: string[] = [] } new Cart().items;
  • class Cart { private items: string[] = [] } class Wishlist extends Cart { first() { return this.items[0]; } }
  • class Order { readonly id = 7 } new Order().id = 8;
  • class Account { private token = "abc" } new Account()["token"];
  • class Account { #token = "abc" } new Account().#token;
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

readonly stops you giving a field a new value. It doesn't stop changes inside the value:

A readonly list that still growsPop out in the code editor (opens in a new tab)TypeScript
class Cart {  readonly items: string[] = [];} const cart = new Cart();cart.items.push("Tea");console.log(cart.items.length);

Line 2 makes items readonly. Line 6 doesn't give items a new list; it adds to the same one. So there's no error, and line 7 prints 1. To stop that too, give the list a read-only type: readonly items: readonly string[]. Then push gets Property 'push' does not exist on type 'readonly string[]'.

  • "private hides the value from other code on the page." No. It's removed before the code runs. JSON.stringify and square brackets still show it. Use #private for that.
  • "A subclass can use its base class's private members." No. private keeps subclasses out. Use protected.
  • "Members are private unless I write public." No. They're public unless you write something else.
  • "readonly means nobody can read it." No. Anyone can read a readonly field. It means nobody can give it a new value after the constructor.
  • "readonly makes the whole value unchangeable." No. A readonly list can still grow with push, as the example above shows.
  • "A modifier error stops the code from running." No. Like other type errors, it's a warning for you. Only #private is also guarded by JavaScript while the code runs.

Practice exercises

Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upFind the line TypeScript marks

A wallet with a private balance. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
class Wallet {
  private balance = 20;

  pay(amount: number) {
    this.balance -= amount;
    return this.balance;
  }
}

const wallet = new Wallet();
console.log(wallet.pay(5));
console.log(wallet.balance);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict a private field at runtime

    userId is private. What does line 6 print?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    class Session {  private userId = "u-42";} const session = new Session();console.log(session["userId"]);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGuard the quantity

      Line 14 skips the setter and sets the quantity to 0. Make count private, and fix line 14 so it uses the setter. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      class QuantityField {  count = 1;   get quantity() {    return this.count;  }   set quantity(value: number) {    this.count = Math.max(1, Math.round(value));  }} const field = new QuantityField();field.count = 0;console.log(field.quantity);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeLet the subclass in

        TypeScript marks line 7. Change one word so the subclass can use the nickname. Which modifier did you use?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        class Profile {  private nickname = "Ash";} class AdminProfile extends Profile {  badge() {    return `${this.nickname} (admin)`;  }} console.log(new AdminProfile().badge());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeStop the order id from changing

          Line 6 changes an order's id, and nothing complains. Change line 2 so TypeScript marks the change. Which line does it mark?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          class Order {  constructor(public id: string) {}} const order = new Order("A-7");order.id = "B-2";console.log(order.id);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeKeep the token out of the JSON

            The starter prints the token with the account. Change one line so JSON.stringify leaves it out. What does it print then?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            class Account {  private token = "abc123";   constructor(public name: string) {}} const account = new Account("Asha");console.log(JSON.stringify(account));

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask where the code that uses the member is: in the class, in a subclass, or outside. Then ask whether you mean what TypeScript says, or what happens when the code runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The access modifiers quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which visibility does a class member have when you write no modifier?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which line does TypeScript flag in the DarkTheme code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Theme {
                  protected color = "orange";
                }
                
                class DarkTheme extends Theme {
                  label() {
                    return `dark ${this.color}`;
                  }
                }
                
                const theme = new DarkTheme();
                console.log(theme.label());
                console.log(theme.color);

                Choose an answer to see the explanation.

              3. Question 3 of 7What does the Coupon code print when it runs?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Coupon {
                  private code = "SAVE5";
                }
                
                console.log(new Coupon()["code"]);

                Choose an answer to see the explanation.

              4. Question 4 of 7Who can use a protected member?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which line of Invoice does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Invoice {
                  readonly total: number;
                
                  constructor(total: number) {
                    this.total = total;
                  }
                
                  discount() {
                    this.total = this.total - 1;
                  }
                }

                Choose an answer to see the explanation.

              6. Question 6 of 7What does JSON.stringify print for this User?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class User {
                  #password = "tea4two";
                
                  constructor(public name: string) {}
                }
                
                console.log(JSON.stringify(new User("Asha")));

                Choose an answer to see the explanation.

              7. Question 7 of 7What's the main difference between private and #private?

                Choose an answer to see the explanation.

              Key takeaways

              • public is the default: any code may use the member. protected allows the class and its subclasses. private allows only the class.
              • Modifiers work on methods, on fields, and in parameter properties like constructor(private name: string).
              • readonly fields get their value where they're declared or in the constructor. Anyone may read them; nobody may give them a new value.
              • private, protected, and readonly are checked by TypeScript and removed before the code runs.
              • JavaScript's #private fields stay private while the code runs: JSON.stringify doesn't see them, and using one outside its class is a SyntaxError.

              Remember the one-liner.
              private is a sign TypeScript reads; #private is a lock JavaScript keeps.

              Coming next: implements & abstract classes. It shows how to promise that a class has a shape, and how to write base classes that subclasses must finish.

              CompleteFrontend Clear concepts. Working examples.