- Declare class fields with their types
- Choose correctly between
public,private,protectedandreadonly - Check a class against an interface with
implementsand use parameter properties - Choose between an
enumand 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.
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`);Aysel: 150 AZN Aysel: 1000000 AZN
Access modifiers
| Modifier | Who can access it |
|---|---|
public | everyone; this is the default when nothing is written |
private | only code inside the same class |
protected | the class itself and the classes derived from it |
readonly | everyone can read it; it can be set only in its declaration or in the constructor |
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';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';
~~~~~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:
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);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:
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;
}
}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 {
~~~~~~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));circle 12.57
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.
enum Direction {
Up,
Down,
Left,
Right,
}
enum Status {
Active = 'ACTIVE',
Blocked = 'BLOCKED',
}
console.log(Direction.Left);
console.log(Direction[2]);
console.log(Status.Blocked);2 Left BLOCKED
Direction[2] returns the name.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:
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');status.ts:15:7 - error TS2345: Argument of type '"banned"' is not assignable to parameter of type 'Status'.
15 label('banned');
~~~~~~~~| Aspect | enum | Union of literal types |
|---|---|---|
| At runtime | a real object is created | completely erased |
| Writing a value | Status.Blocked | 'blocked' |
node file.ts (type stripping) | not supported | works |
| Getting all values as a list | possible 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.
publicmeans everyone,privateonly the class,protectedthe class and its subclasses,readonlyread-only.privateis checked only at compile time; for runtime privacy there are#fields.implementschecks 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.