Skip to content
Educora
Intermediate17 min6 / 8

Classes and enums

Add typed fields to classes, control access with `public`, `private`, `protected` and `readonly`, check a class against an interface with `implements`, and compare `enum` with unions of literal types.

Check yourself
In this lesson you will learn
  • Declare class fields with their types
  • Choose correctly between public, private, protected and readonly
  • Check a class against an interface with implements and use parameter properties
  • Choose between an enum and a union of literal types

In the JavaScript course we wrote a BankAccount class. It had a weak spot: any code could write account.balance = 1000000, and nothing stopped it. TypeScript adds three things to classes: types for fields, access modifiers that decide who may touch what, and a check that a class matches an interface. At the end of the lesson we will compare two ways to describe a fixed set of values: an enum and a union of literal types.

Typed fields

In JavaScript it was enough to create a field in the constructor with this.owner = owner. In TypeScript every field must be declared in the class body together with its type. If you don't declare it, the compiler reports error TS2339: Property 'owner' does not exist on type 'BankAccount'. Method parameters and return values are typed just like in ordinary functions.

TypeScript
class BankAccount {
  owner: string;
  balance: number;

  constructor(owner: string, balance = 0) {
    this.owner = owner;
    this.balance = balance;
  }

  deposit(amount: number): void {
    this.balance += amount;
  }
}

const account = new BankAccount('Aysel', 100);
account.deposit(50);
console.log(`${account.owner}: ${account.balance} AZN`);

account.balance = 1000000;
console.log(`${account.owner}: ${account.balance} AZN`);
Expected output
Aysel: 150 AZN
Aysel: 1000000 AZN
The types are fine, but the last two lines show the problem: the balance can be set to anything from outside.

Access modifiers

ModifierWho can access it
publiceveryone; this is the default when nothing is written
privateonly code inside the same class
protectedthe class itself and the classes derived from it
readonlyeveryone can read it; it can be set only in its declaration or in the constructor
TypeScript
class BankAccount {
  readonly owner: string;
  private balance: number;

  constructor(owner: string, balance = 0) {
    this.owner = owner;
    this.balance = balance;
  }
}

const account = new BankAccount('Aysel', 100);
account.balance = 1000000;
account.owner = 'Murad';
Text
account.ts:12:9 - error TS2341: Property 'balance' is private and only accessible within class 'BankAccount'.

12 account.balance = 1000000;
           ~~~~~~~

account.ts:13:9 - error TS2540: Cannot assign to 'owner' because it is a read-only property.

13 account.owner = 'Murad';
           ~~~~~
Now only the class's own methods can change the balance, and the owner's name can't change after the account is opened.

A protected field is hidden from the outside, but derived classes can see it. A savings account needs the parent's balance to calculate interest:

TypeScript
class Account {
  protected balance = 0;

  deposit(amount: number): void {
    this.balance += amount;
  }
}

class SavingsAccount extends Account {
  addInterest(rate: number): number {
    this.balance += (this.balance * rate) / 100;
    return this.balance;
  }
}

const leyla = new SavingsAccount();
leyla.deposit(1000);
console.log(leyla.addInterest(5));
console.log(leyla.balance);
Text
savings.ts:19:19 - error TS2445: Property 'balance' is protected and only accessible within class 'Account' and its subclasses.

19 console.log(leyla.balance);
                     ~~~~~~~
this.balance is allowed inside SavingsAccount, but leyla.balance outside is not.

implements and parameter properties

Writing class Square implements Shape asks the compiler: “check that this class has everything the Shape interface requires”. If you forget a method, the error is reported right at the class, not somewhere the interface is used:

TypeScript
interface Shape {
  name: string;
  area(): number;
}

class Circle implements Shape {
  name = 'circle';
  radius: number;

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

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square implements Shape {
  name = 'square';
  side: number;

  constructor(side: number) {
    this.side = side;
  }
}
Text
shapes.ts:19:7 - error TS2420: Class 'Square' incorrectly implements interface 'Shape'.
  Property 'area' is missing in type 'Square' but required in type 'Shape'.

19 class Square implements Shape {
         ~~~~~~
TypeScript
class Circle {
  constructor(private radius: number, public label = 'circle') {}

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

const c = new Circle(2);
console.log(c.label, c.area().toFixed(2));
Expected output
circle 12.57
Two fields, radius and label, are declared and filled in a single line.

enum or literal types?

An enum is a set of related named constants. If you don't give values, the members are numbered starting from 0; you can also give them string values. An enum is one of the rare TypeScript features that produces JavaScript code besides types: the compiler turns it into a real object.

TypeScript
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

enum Status {
  Active = 'ACTIVE',
  Blocked = 'BLOCKED',
}

console.log(Direction.Left);
console.log(Direction[2]);
console.log(Status.Blocked);
Expected output
2
Left
BLOCKED
A numeric enum also works in reverse: Direction[2] returns the name.
JavaScript
var Status;
(function (Status) {
    Status["Active"] = "ACTIVE";
    Status["Blocked"] = "BLOCKED";
})(Status || (Status = {}));
tsc produces this JavaScript from enum Status — the enum is not erased, it stays in the program.

Most of the time a union of literal types does the same job. It is completely erased, the values are plain strings, and the compiler catches mistakes just as well:

TypeScript
type Status = 'active' | 'blocked' | 'deleted';

function label(status: Status): string {
  switch (status) {
    case 'active':
      return 'Active user';
    case 'blocked':
      return 'Blocked';
    case 'deleted':
      return 'Deleted';
  }
}

console.log(label('blocked'));
label('banned');
Text
status.ts:15:7 - error TS2345: Argument of type '"banned"' is not assignable to parameter of type 'Status'.

15 label('banned');
         ~~~~~~~~
AspectenumUnion of literal types
At runtimea real object is createdcompletely erased
Writing a valueStatus.Blocked'blocked'
node file.ts (type stripping)not supportedworks
Getting all values as a listpossible with Object.values(Status)not possible directly from the type

Key points

  • In TypeScript every class field is declared in the body together with its type.
  • public means everyone, private only the class, protected the class and its subclasses, readonly read-only.
  • private is checked only at compile time; for runtime privacy there are # fields.
  • implements checks a class against an interface; constructor(private x: T) is a shortcut for declaring a field.
  • An enum creates a real JavaScript object; a union of literal types is erased and is usually the better choice.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Which modifier allows access to a field only from the class itself and its subclasses?