Free Handbook · Runs in your browser

Classes & OOP

TypeScript classes are JavaScript classes plus compile-time guarantees: access modifiers, readonly fields, abstract members and interfaces that a class must actually satisfy.

0 / 105 lessons🔥 0 day streak
ShareXLinkedIn

Module 05 · what you'll be able to do

  • Write a class with typed fields, a constructor and methods
  • Use public, private, protected and readonly to control what other code may touch
  • Skip constructor boilerplate with parameter properties
  • Model a shared contract with abstract classes and with implements
  • Add static members and get/set accessors to a class
01

Class basics: fields, constructor, methods

A TypeScript class is a JavaScript class with types layered on top. You still write class, constructor and methods exactly as in JavaScript — the only new thing is annotating what type each field, parameter and return value is, so the compiler can catch a wrong assignment before the code ever runs.

  • Field declarationstitle: string at the top of the class body declares a property and its type, before it is ever assigned.
  • Constructor — runs once, when new ClassName(...) is called, and is the usual place to assign fields from the arguments it receives.
  • Methods — ordinary functions attached to the class, with typed parameters and a typed return value like any other function.
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
"Dune" by Frank Herbert
Your turn
Add a third field, year: number, assign it in the constructor, and include it in the string describe() returns.
Field declarations are optional if you assign in the constructor
With strict mode on, a field like title: string must either have a default value, be assigned in every constructor path, or be declared with the parameter-property shorthand you will see later in this module. Declare-then-assign, as above, is the pattern to reach for first — it is explicit about the shape of the class before you read a single line of the constructor.
Class
A blueprint for creating objects that share the same fields and methods.
Instance
One object created from a class with new — each call to new Book(...) makes a separate instance.
Field
A typed property declared on the class, one per instance.
Method
A function attached to the class, called on an instance with instance.method().
02

Access modifiers and readonly fields

By default every field and method on a TypeScript class is public — readable and callable from anywhere. Three modifiers narrow that: private restricts a member to the class it is declared in, protected extends that same restriction to subclasses, and readonly allows a field to be set once (at declaration or inside the constructor) and never reassigned after that.

ModifierVisible fromTypical use
public (default)AnywhereThe class’s normal API — methods and fields other code is meant to use.
privateOnly inside the same classInternal state a caller has no business touching, like a cache or a counter.
protectedThe class and its subclassesState or helpers a base class exposes to children but not to the outside world.
readonlySet once, read anywhere it is otherwise visibleValues that must never change after construction, like an ID or a creation date.
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
Rex barks
Labrador
Your turn
Try adding console.log(d.name) after the existing lines. It still runs here (this editor only strips types, it does not type-check), but paste the same code into the TypeScript Playground and read the error protected produces from outside the class.
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
AC1001 650
Your turn
A readonly field can still be mutated internally through a method that changes some other field — only reassigning accountNumber itself, from anywhere, is blocked. Confirm that by trying acc.accountNumber = "AC9999" in the compiler.
private and protected are erased at compile time
None of these modifiers exist in the JavaScript TypeScript emits — a private field is an ordinary, fully readable property once compiled. The check is entirely the compiler refusing to let your own code reference it incorrectly; anyone with the compiled .js file (or a debugger) can still read it. If you need privacy enforced at runtime, use JavaScript’s native #field syntax instead.
Error you will hit

TS2341: accessing a private field from outside the class

typescript
class Wallet {
  private balance = 0
}
const w = new Wallet()
console.log(w.balance)
app.ts:5:15 - error TS2341: Property 'balance' is private and only accessible within class 'Wallet'.

5 console.log(w.balance)
                ~~~~~~~
Why the compiler said that

balance is declared private, which restricts it to code written inside Wallet itself. The call on line 5 is outside the class, so the compiler refuses to let it read the field, even though at runtime the property is perfectly readable.

The fix

Expose a public method that returns the value instead of the field itself.

typescript
class Wallet {
  private balance = 0
  getBalance(): number {
    return this.balance
  }
}
const w = new Wallet()
console.log(w.getBalance())
JuniorWhat happens to TypeScript's private and protected keywords when the code compiles to JavaScript?

They are erased. tsc enforces access rules only while checking your TypeScript source; the emitted JavaScript has an ordinary property that anything can read or write. Real, runtime-enforced privacy requires the native #field syntax instead.

What they are really testing: Whether the candidate understands that private is a compile-time-only guarantee, not a runtime one.

Quick check

In the compiled JavaScript output, which TypeScript access modifier still restricts access at runtime?

03

Parameter properties: the constructor shorthand

Declaring a field and then assigning it in the constructor is common enough that TypeScript has a shortcut: put an access modifier (public, private, protected or readonly) directly in front of a constructor parameter, and the compiler both declares a field of that name AND assigns the argument to it — one line does what would otherwise take two.

Without parameter properties

  • class Employee {
  • name: string
  • private salary: number
  • constructor(name: string, salary: number) {
  • this.name = name
  • this.salary = salary
  • }
  • }

With parameter properties

  • class Employee {
  • constructor(
  • public name: string,
  • private salary: number,
  • ) {}
  • }
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
Nia EMP-204 65000
Your turn
Add a fourth parameter property, public department: string = "General", with a default value, and print it too.
VisualizeWhat a parameter property actually does at the call siteStep 1 / 5
class Employee {
constructor(
public name: string,
private salary: number,
) {}
raise(amount: number): void {
this.salary += amount
}
}
const e = new Employee("Nia", 60000)
e.raise(5000)
console.log(e.name)
Line 10

new Employee("Nia", 60000) runs the constructor. Because name and salary each carry a modifier keyword, the compiler also generates this.name = name and this.salary = salary inside the constructor body, ahead of anything you wrote there yourself.

Variables now
name'Nia'
salary60000
All 5 steps as a table
StepLineWhat happenedVariables now
110new Employee("Nia", 60000) runs the constructor. Because name and salary each carry a modifier keyword, the compiler also generates this.name = name and this.salary = salary inside the constructor body, ahead of anything you wrote there yourself.name = 'Nia' salary = 60000
23public name: string is the field declaration and the assignment in one place: e.name is now set.e.name = 'Nia'
34private salary: number does the same, but the field it created is only reachable from inside Employee after the compiler checks your code — at runtime it is a completely ordinary property.e.salary = 60000
411e.raise(5000) runs, adding 5000 to the private field.e.salary = 65000
512Prints the public field.
Error you will hit

TS2564: a field with no initializer and no assignment

typescript
class Order {
  total: number
  constructor(items: number[]) {
    console.log(items.length)
  }
}
app.ts:2:3 - error TS2564: Property 'total' has no initializer and is not definitely assigned in the constructor.

2   total: number
    ~~~~~
Why the compiler said that

Under strict mode, every declared field must be assigned before the constructor finishes running — either with a default value or in every code path through the constructor. total is declared but never touched, so the compiler cannot prove it will hold a number by the time an Order exists.

The fix

Give the field a default value, or assign it inside the constructor.

typescript
class Order {
  total: number = 0
  constructor(items: number[]) {
    this.total = items.reduce((sum, n) => sum + n, 0)
  }
}
Mid-levelWhat does constructor(private x: number) do differently from writing constructor(x: number) { this.x = x } yourself?

Nothing behaviorally — it is pure shorthand. A modifier keyword in front of a constructor parameter tells the compiler to both declare a class field of that name with that access level, and assign the incoming argument to it, automatically, before the rest of the constructor body runs. The emitted JavaScript is the same either way.

What they are really testing: Familiarity with parameter properties, and whether the candidate knows it is sugar, not a different mechanism.

04

Abstract classes and abstract methods

An abstract class is a base class that can never be instantiated directly — only extended. It can mix abstract methods, which declare a signature but no body and must be implemented by every subclass, with ordinary concrete methods that subclasses inherit as-is. This is the classic template method pattern: the base class defines the overall shape, subclasses fill in the missing piece.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
Charged via credit card for $75
Your turn
Add a second subclass, BankTransfer, implementing charge() differently, and call .receipt(75) on an instance of it too.
Abstract class vs. plain base class
A plain base class can be instantiated on its own, even if that rarely makes sense. Marking it abstract makes that a compile error — PaymentMethod above is a contract that promises charge() exists, not a payment method you would ever create by itself.
Error you will hit

TS2511: instantiating an abstract class directly

typescript
abstract class PaymentMethod {
  abstract charge(amount: number): string
}
const pm = new PaymentMethod()
app.ts:4:15 - error TS2511: Cannot create an instance of an abstract class.

4 const pm = new PaymentMethod()
                ~~~~~~~~~~~~~
Why the compiler said that

PaymentMethod is declared abstract specifically to forbid this — an abstract class describes a shape a subclass must complete, and charge() has no body here to run.

The fix

Instantiate a concrete subclass instead, one that actually implements charge().

typescript
class CreditCard extends PaymentMethod {
  charge(amount: number): string {
    return `credit card for $${amount}`
  }
}
const pm = new CreditCard()
Mid-levelWhen would you reach for an abstract class instead of an interface?

When subclasses need to share real implementation — a base constructor, fields, or concrete helper methods — in addition to a contract. An interface is a pure shape with zero implementation of its own; an abstract class can mix abstract members (subclasses must implement) with concrete ones (subclasses inherit unchanged), which is exactly what a template method pattern needs.

What they are really testing: Whether the candidate can explain the practical difference, not just recite the syntax.

05

implements: a class fulfilling an interface

An interface describes a shape with no implementation at all — just the fields and method signatures a type must have. A class can declare implements SomeInterface, and the compiler checks that the class really does provide every member the interface lists, with a compatible type for each one.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
{"name":"Mug","price":12}
Your turn
Add a second interface, HasId { id: number }, and make Product implement both: implements Serializable, HasId.

interface

  • No implementation, ever
  • A class can implement several at once
  • Also usable as a plain object type
  • Cannot hold static members

abstract class

  • Can mix abstract and concrete members
  • A class can extend only one
  • Only usable via a class hierarchy
  • Can hold static members
Error you will hit

TS2420: implements without providing every member

typescript
interface Serializable {
  serialize(): string
}
class Product implements Serializable {
  constructor(public name: string) {}
}
app.ts:4:7 - error TS2420: Class 'Product' incorrectly implements interface 'Serializable'.
  Property 'serialize' is missing in type 'Product' but required in type 'Serializable'.

4 class Product implements Serializable {
        ~~~~~~~
Why the compiler said that

implements is a promise the class keeps every member of the interface. Serializable requires a serialize() method, and Product never defines one, so the compiler refuses to accept that the promise is kept.

The fix

Add the missing method with a compatible signature.

typescript
class Product implements Serializable {
  constructor(public name: string) {}
  serialize(): string {
    return JSON.stringify({ name: this.name })
  }
}
SeniorA class implements two interfaces that each declare a method with the same name but incompatible signatures. What happens?

The class only compiles if a single method implementation can structurally satisfy both interfaces at once — sometimes possible by widening a parameter to a union or a return type to an intersection. If no single signature can satisfy both, the class cannot implement both interfaces as written; one of them has to change, or the class has to pick one and adapt to the other through a separate wrapper.

What they are really testing: Understanding structural typing at the boundary of multiple interface implementations, an edge case most junior developers never hit.

Quick check

What is the key difference between an interface and an abstract class in TypeScript?

06

Static members, getters and setters

A static member belongs to the class itself, not to any one instance — there is exactly one copy, shared across every object created from that class. get and set accessors let a piece of code that reads or writes like a plain field actually run a method behind the scenes, useful for computed values or validation on assignment.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
1 2
1
Your turn
Add a static method IdGenerator.count(): number that returns how many IDs have been handed out so far (without resetting anything), and log it.
typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
25 77
Your turn
Add a setter for fahrenheit that converts back and assigns this._celsius, so t.fahrenheit = 32 makes t.celsius read 0.
Accessors read and write like fields, but run code
From the outside, t.celsius = 25 and t.celsius look exactly like touching a plain field — the caller never writes (). Internally, TypeScript compiles get/set pairs to Object.defineProperty, so a setter can validate or transform a value on the way in, and a getter can compute one on the way out, invisibly to the caller.
Static member
A field or method that belongs to the class itself, one copy shared by every instance, accessed as ClassName.member.
Getter
A get method called with property syntax (no parentheses) that computes or returns a value.
Setter
A set method called with assignment syntax (property = value) that runs code instead of a plain assignment.

Finish the TypeScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.