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.
- 01Choose who can use a memberMark fields and methods
public,protected, orprivate, and predict which lines TypeScript marks. - 02Stop changes with readonlyMake a field that is set once in the constructor, and know what
readonlydoesn't stop. - 03Pick private or #privateExplain why TypeScript's
privateis gone when the code runs, and when JavaScript's#privateis 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:
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:
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.
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.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:
publicmembers, which any code may use - In real life: The back room, for staff and staff in training
- In TypeScript:
protectedmembers, for the class and its subclasses - In real life: The manager's office, for the manager only
- In TypeScript:
privatemembers, for the class itself - In real life: A price tag printed once and never changed
- In TypeScript: A
readonlyfield, set once in the constructor
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:
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:
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:
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);3, 4public price = 4;No errorspublic 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.
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.
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 wants to use the member? | public | protected | private |
|---|---|---|---|
| Code inside the class itself | Yes. | Yes. | Yes. |
| Code inside a subclass | Yes. | Yes. | No. |
| Code outside the classes | Yes. | 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:
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.
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.
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:
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:
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:
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.
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.
JSON.stringify(account) while the code runs. A private field is an ordinary property by then, so it shows. A #private field stays hidden.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.
| Question | private 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:
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}`);Asha pays $11cart.add({ name: "Free cake", price: -4 });No errorsadd 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.
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.
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;
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
readonly stops you giving a field a new value. It doesn't stop changes inside the value:
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.stringifyand square brackets still show it. Use#privatefor that. - "A subclass can use its base class's private members." No.
privatekeeps subclasses out. Useprotected. - "Members are private unless I write public." No. They're
publicunless 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
#privateis 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.
A wallet with a private balance. Which line will TypeScript mark?
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);class Wallet { private balance = 20; pay(amount: number) { this.balance -= amount; return this.balance; }} const wallet = new Wallet();console.log(wallet.pay(5));Line 12 reads wallet.balance from outside the class, so TypeScript reports Property 'balance' is private and only accessible within class 'Wallet'. Line 11 is fine: pay is public, and it uses balance from inside. Removing line 12 leaves code that prints 15.
userId is private. What does line 6 print?
class Session { private userId = "u-42";} const session = new Session();console.log(session["userId"]);class Session { private userId = "u-42";} const session = new Session();console.log(session["userId"]); // u-42: private is gone when the code runsThere's no error, because TypeScript allows square brackets for a private member. When the code runs, userId is an ordinary property, so it prints u-42.
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?
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);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 = 0;console.log(field.quantity);With private count, line 14 can't write count directly. It uses the setter instead, which keeps the value at least 1, so the code prints 1 instead of 0.
TypeScript marks line 7. Change one word so the subclass can use the nickname. Which modifier did you use?
class Profile { private nickname = "Ash";} class AdminProfile extends Profile { badge() { return `${this.nickname} (admin)`; }} console.log(new AdminProfile().badge());class Profile { protected nickname = "Ash";} class AdminProfile extends Profile { badge() { return `${this.nickname} (admin)`; }} console.log(new AdminProfile().badge());protected lets subclasses use nickname while still keeping outside code out. Line 7 has no error, and the code prints Ash (admin).
Line 6 changes an order's id, and nothing complains. Change line 2 so TypeScript marks the change. Which line does it mark?
class Order { constructor(public id: string) {}} const order = new Order("A-7");order.id = "B-2";console.log(order.id);class Order { constructor(readonly id: string) {}} const order = new Order("A-7");order.id = "B-2";console.log(order.id);With readonly id, line 6 gets Cannot assign to 'id' because it is a read-only property. The id is still public, so line 7 can read it. TypeScript doesn't stop the code from running, so it still prints B-2 until you remove line 6.
The starter prints the token with the account. Change one line so JSON.stringify leaves it out. What does it print then?
class Account { private token = "abc123"; constructor(public name: string) {}} const account = new Account("Asha");console.log(JSON.stringify(account));class Account { #token = "abc123"; constructor(public name: string) {}} const account = new Account("Asha");console.log(JSON.stringify(account));#token is JavaScript's own private field, so JSON.stringify doesn't see it, and the code prints {"name":"Asha"}. With private token, it printed the token too.
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.
Question 1 of 7Which visibility does a class member have when you write no modifier?
Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in the DarkTheme code?
Read the code, then predictclass 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.
Question 3 of 7What does the Coupon code print when it runs?
Read the code, then predictclass Coupon { private code = "SAVE5"; } console.log(new Coupon()["code"]);Choose an answer to see the explanation.
Question 4 of 7Who can use a protected member?
Choose an answer to see the explanation.
Question 5 of 7Which line of Invoice does TypeScript flag?
Read the code, then predictclass Invoice { readonly total: number; constructor(total: number) { this.total = total; } discount() { this.total = this.total - 1; } }Choose an answer to see the explanation.
Question 6 of 7What does JSON.stringify print for this User?
Read the code, then predictclass User { #password = "tea4two"; constructor(public name: string) {} } console.log(JSON.stringify(new User("Asha")));Choose an answer to see the explanation.
Question 7 of 7What's the main difference between private and #private?
Choose an answer to see the explanation.
Key takeaways
publicis the default: any code may use the member.protectedallows the class and its subclasses.privateallows only the class.- Modifiers work on methods, on fields, and in parameter properties like
constructor(private name: string). readonlyfields get their value where they're declared or in the constructor. Anyone may read them; nobody may give them a new value.private,protected, andreadonlyare checked by TypeScript and removed before the code runs.- JavaScript's
#privatefields stay private while the code runs:JSON.stringifydoesn't see them, and using one outside its class is aSyntaxError.
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.