TypeScript · Lesson 6 of 12
Classes, Access Modifiers and Abstract Classes
Write TypeScript classes with typed fields, public, private, protected and readonly modifiers, parameter properties, interfaces and abstract classes.
- Intermediate
- 15 min read
- 3 objectives
Before this lessonLesson 5: Generics and Utility Types
What you will learn
- Declare typed fields and constructors
- Choose between private, #private, protected and readonly
- Implement interfaces and extend abstract classes
Your Progress
0 of 12 lessons 0%
- Lessons0 / 12
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
JavaScript has had classes since 2015. TypeScript adds types for their fields and methods, plus a few keywords that control who can read or change what. Classes are not required in TypeScript (many codebases use plain objects and functions), but you will meet them in frameworks like Angular and NestJS, in SDKs, and anywhere state and behaviour belong together.
Fields, constructors and methods
In TypeScript you declare fields at the top of the class with their types. Under strict mode, every field must be definitely assigned: either with an initializer or in the constructor. That rule catches the classic bug of reading a property that was never set.
class Cart {
items: { sku: string; price: number }[] = []; // initializer
owner: string;
constructor(owner: string) {
this.owner = owner; // assigned in constructor
}
add(sku: string, price: number): this {
this.items.push({ sku, price });
return this; // returning this enables chaining
}
get total(): number { // a getter reads like a field
return this.items.reduce((sum, i) => sum + i.price, 0);
}
}
const cart = new Cart("Ada").add("pen", 3).add("book", 12);
cart.total; // 15Here is the same class in JavaScript, which is exactly what the TypeScript compiles to once types are removed:
class Cart {
items = [];
constructor(owner) {
this.owner = owner;
}
add(sku, price) {
this.items.push({ sku, price });
return this;
}
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0);
}
}
const cart = new Cart("Ada").add("pen", 3).add("book", 12);
console.log(cart.owner, cart.items.length, cart.total);Ada 2 15
Access modifiers: public, private, protected
Members are public by default. Mark implementation details private (only this class can use them) or protected (this class and subclasses). Hiding internals means you can change them later without breaking code that uses the class.
class BankAccount {
public readonly id: string;
private balance = 0;
protected history: number[] = [];
constructor(id: string) {
this.id = id;
}
deposit(amount: number) {
if (amount <= 0) throw new Error("Amount must be positive");
this.balance += amount;
this.history.push(amount);
}
getBalance() {
return this.balance;
}
}
const acct = new BankAccount("acc_1");
acct.deposit(100);
// acct.balance = 1_000_000; // Error: Property 'balance' is private
// acct.id = "other"; // Error: Cannot assign to 'id' because it is a read-only propertyreadonly means a field can only be assigned in its declaration or the constructor. It is a good default for IDs and configuration that should never change after creation.
class Counter {
#count = 0; // truly private at runtime
increment() {
this.#count++;
return this.#count;
}
}
const c = new Counter();
c.increment();
console.log(c.increment());
console.log(c.count, Object.keys(c).length);2 undefined 0
Parameter properties
Declaring a field, taking a constructor parameter and assigning one to the other is so common that TypeScript has a shortcut. Put a modifier (private, public, protected or readonly) on a constructor parameter and it becomes a field automatically.
class EmailService {
constructor(
private readonly apiKey: string,
public readonly from = "noreply@stackcone.com",
) {}
send(to: string, subject: string) {
return { from: this.from, to, subject, auth: this.apiKey.slice(0, 4) + "..." };
}
}This style is everywhere in Angular and NestJS for dependency injection. One caution: parameter properties are TypeScript-only syntax that generates runtime code, so they are rejected when you enable erasableSyntaxOnly (used when Node.js runs .ts files by simply stripping types). If your project uses that flag, write fields out explicitly.
Implementing interfaces
A class can promise to match one or more interfaces with implements. The compiler then checks that every required member exists with a compatible type. This lets other code depend on the interface rather than a concrete class, which makes swapping implementations (for example a fake in tests) easy.
interface Storage {
get(key: string): string | undefined;
set(key: string, value: string): void;
}
class MemoryStorage implements Storage {
#data = new Map<string, string>();
get(key: string) { return this.#data.get(key); }
set(key: string, value: string) { this.#data.set(key, value); }
}
function saveTheme(store: Storage, theme: "light" | "dark") {
store.set("theme", theme); // works with any Storage implementation
}Note that implements only checks; it does not change the class's type or add anything. TypeScript's type system is structural, so any object with matching get and set methods could be passed to saveTheme, with or without the keyword.
Abstract classes
An abstract class is a base class that cannot be instantiated on its own. It can contain shared, working code plus abstract members that each subclass must provide. Use it when subclasses share real behaviour; if you only need a contract, an interface is simpler.
abstract class Notifier {
constructor(protected readonly appName: string) {}
abstract deliver(to: string, text: string): Promise<void>;
async notify(to: string, text: string) {
await this.deliver(to, `[${this.appName}] ${text}`); // shared logic
}
}
class SmsNotifier extends Notifier {
async deliver(to: string, text: string) {
console.log(`SMS to ${to}: ${text}`);
}
}
// new Notifier("x"); // Error: Cannot create an instance of an abstract class
const sms = new SmsNotifier("stackcone");
await sms.notify("+91 90000 00000", "Your order shipped");When a subclass overrides a method, add the override keyword and enable noImplicitOverride in tsconfig. Then if the base method is renamed, the compiler flags every stale override instead of silently leaving an orphan method.
Recap
- Declare typed fields; strict mode requires each to be initialized or assigned in the constructor.
private/protectedare compile-time checks;#fieldis real runtime privacy.readonlyfields can only be set at creation; parameter properties shorten constructor boilerplate.implementschecks a class against an interface; code should depend on the interface.- Abstract classes share implementation and force subclasses to fill in
abstractmembers.
// Write your solution here
Finished reading? Mark this lesson complete to track your progress.
