Skip to content
wannacode

Data, switch and destructuring

Problem

The seat is taken, but Map cannot find it

Movie theater reservations: the key is the seat (row and number), the value is the person's name.

Seat
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
}
}
const reservations = new Map();
reservations.set(new Seat(5, 12), "Maria");
console.log(reservations.get(new Seat(5, 12)));
console.log(reservations.has(new Seat(5, 12)));
Output
undefined
false
Maria is in row 5, seat 12, but get returns undefined. new Seat(5, 12) written twice is two different objects, and a Map compares object keys by whether they are the same object. JavaScript has no methods, like equals and hashCode in Java, that let a class say when two objects are equal.

The idea

A class for data and a key built from the fields

JavaScript has no record. What Java writes by itself, here you write yourself:
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
Object.freeze(this);
}
key() {
return `${this.row}-${this.number}`;
}
toString() {
return `Seat(${this.row}, ${this.number})`;
}
}
const reservations = new Map();
reservations.set(new Seat(5, 12).key(), "Maria");
console.log(reservations.get(new Seat(5, 12).key()));
console.log(reservations.has(new Seat(5, 12).key()));
const seat = new Seat(5, 12);
console.log(`${seat}`);
console.log(`Row ${seat.row}, seat ${seat.number}`);
Output
Maria
true
Seat(5, 12)
Row 5, seat 12
  • Object.freeze(this) at the end of the constructor forbids changing the fields.
  • key() makes a string from the fields: "5-12". Two strings with the same characters are equal, so the string is a good key for a Map, and get now finds Maria.
  • toString() says what the object looks like as text.

You read the fields directly: seat.row.

const reservations = new Map();
reservations.set(new Seat(5, 12).key(), "Maria");
console.log(reservations.get(new Seat(5, 12).key()));
console.log(reservations.has(new Seat(5, 12).key()));
const seat = new Seat(5, 12);
console.log(`${seat}`);
console.log(`Row ${seat.row}, seat ${seat.number}`);
Step by step, in words
  1. An empty Map: the key is the theater seat as a string, the value is a name.
  2. set adds the key "5-12", built from Seat(5, 12), with the value "Maria".
  3. get receives the key of a new Seat object. The object is a different one, not the one from set.
  4. key() builds the string from the fields, so it is the same: "5-12".
  5. The two strings are built from equal fields: row 5 and 5, seat 12 and 12. They are equal, and get returns "Maria".
  6. has does the same with the key of yet another new Seat: true.
  7. Here toString is written by hand, and the fields are read directly: seat.row.
More precisely, if you are curious
  • Changing a frozen field does not go through: seat.row stays 5. In a regular file there is no error, and the slip is hard to catch. The immutability is also only one level deep: an array in a field can change.
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
Object.freeze(this);
}
key() {
return `${this.row}-${this.number}`;
}
toString() {
return `Seat(${this.row}, ${this.number})`;
}
}
const seat = new Seat(5, 12);
seat.row = 6;
console.log(seat.row);
class Cart {
constructor(items) {
this.items = items;
Object.freeze(this);
}
}
const cart = new Cart(["coffee"]);
cart.items.push("honey");
console.log(cart.items);
Output
5
[ 'coffee', 'honey' ]
  • In strict mode the same change stops with a TypeError. Strict mode covers the code inside a class, a file that starts with "use strict";, and a module with import:
"use strict";
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
Object.freeze(this);
}
key() {
return `${this.row}-${this.number}`;
}
toString() {
return `Seat(${this.row}, ${this.number})`;
}
}
const seat = new Seat(5, 12);
try {
seat.row = 6;
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Output
TypeError: Cannot assign to read only property 'row' of object '[object Object]'

The idea

Checks and methods

A price cannot be negative. The check is at the start of the constructor, before the fields are set.
class Price {
constructor(amount, currency) {
if (amount < 0) {
throw new RangeError(`The price cannot be negative: ${amount}`);
}
this.amount = amount;
this.currency = currency;
Object.freeze(this);
}
withDiscount(percent) {
return new Price(this.amount * (1 - percent / 100), this.currency);
}
toString() {
return `Price(${this.amount} ${this.currency})`;
}
}
const coffee = new Price(18.5, "EUR");
const sale = coffee.withDiscount(20);
console.log(`${coffee}`);
console.log(`${sale}`);
try {
new Price(-5, "EUR");
} catch (e) {
console.log(e.message);
}
Output
Price(18.5 EUR)
Price(14.8 EUR)
The price cannot be negative: -5
  • RangeError is the built-in kind of error for a value outside the allowed range. If the constructor throws, the object is not created.
  • withDiscount does not change the price. It returns a new Price. That is how you work with objects that do not change: coffee stays €18.50.

Try it

Are they equal?

Seat is the class with key() from the card before this one.

What will the program print?

const a = new Seat(5, 12);
const b = new Seat(5, 12);
console.log(a === b);
console.log(a.key() === b.key());
console.log(new Set([a, b]).size);
Answer

Try it

The delivery comes out expensive

Delivery to an office is €4.50, but the program says €6.90. Which line is the bug on?

Pick the line with the bug.

The idea

switch with return

JavaScript has no switch expression that gives a value. So the switch goes into a function, and every case ends with return.
function deliveryPrice(method) {
switch (method) {
case "office":
return 4.5;
case "courier":
case "address":
return 6.9;
case "store":
return 0;
default:
throw new Error(`Unknown delivery method: ${method}`);
}
}
const price = deliveryPrice("office");
console.log(`Delivery: €${price}`);
Output
Delivery: €4.5
  • return leaves the function, so after it there is no fall-through, and you do not write break.
  • Two cases with the same code are written one under the other: case "courier": is empty and falls through into case "address":. That is the useful kind of fall-through.
  • default catches the rest. Here it throws an exception.
  • JavaScript does not check that every input is covered. Without default, no case matches an unknown method: the function reaches its end and returns undefined, with no error:
function deliveryPrice(method) {
switch (method) {
case "office":
return 4.5;
case "courier":
return 6.9;
}
}
console.log(deliveryPrice("post"));
Output
undefined

If a case has several lines and its own const, it goes in { }, so they do not mix with the other cases:

function delivery(method, weightKg) {
switch (method) {
case "office":
return 4.5;
case "courier": {
const base = 6.9;
return weightKg > 10 ? base + 3 : base;
}
default:
return 0;
}
}
console.log(delivery("courier", 4));
console.log(delivery("courier", 12));
Output
6.9
9.9
More precisely, if you are curious

When every case is just a value, you can use an object as a lookup table instead of a switch. ?? gives the fallback for a key that is not there:

const PRICES = { "office": 4.5, "courier": 6.9, "store": 0 };
console.log(PRICES["courier"]);
console.log(PRICES["post"] ?? "unknown method");
Output
6.9
unknown method

The idea

A family of types

The shop accepts three payment methods: card, bank transfer and cash on delivery. There are no others. Each method is its own class:

class CardPayment {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
}
class BankTransfer {
constructor(iban) {
this.iban = iban;
}
}
class CashOnDelivery {}

Each payment method is just data: a card number, an IBAN or nothing. CashOnDelivery has no fields: its body is empty.

In Java, sealed makes the compiler refuse a fourth type. JavaScript has no way to say "only these three". A fourth class can show up anywhere. So the function that tells the kinds apart has to protect itself from an unknown kind. That is the next card.

See it

instanceof and destructuring

The fee depends on the payment method. JavaScript has no type patterns like the ones in a Java switch. The check is done with instanceof, one after another:
function fee(payment, amount) {
if (payment instanceof CardPayment) {
return amount * 0.015;
}
if (payment instanceof BankTransfer) {
return 0.5;
}
if (payment instanceof CashOnDelivery) {
return 2;
}
throw new TypeError(`Unknown payment: ${payment.constructor.name}`);
}
console.log(fee(new CardPayment("4111222233334444"), 200));
console.log(fee(new BankTransfer("BG80BNBG96611020345678"), 200));
console.log(fee(new CashOnDelivery(), 200));
Output
3
0.5
2

The throw at the end is the safeguard: if the object is none of the three kinds, the program stops with a clear message instead of quietly returning undefined:

class Voucher {}
try {
fee(new Voucher(), 200);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Output
TypeError: Unknown payment: Voucher

The fields are taken out with destructuring: const { iban } = payment; takes the field iban of the object and puts it in a variable with the same name. The condition for a Bulgarian account is a regular ? :.

function describe(payment) {
if (payment instanceof CardPayment) {
const { cardNumber } = payment;
return `card ****${cardNumber.slice(-4)}`;
}
if (payment instanceof BankTransfer) {
const { iban } = payment;
return iban.startsWith("BG") ? "transfer from a Bulgarian account" : "transfer from abroad";
}
if (payment instanceof CashOnDelivery) {
return "cash on delivery";
}
throw new TypeError(`Unknown payment: ${payment.constructor.name}`);
}
console.log(describe(new CardPayment("4111222233334444")));
console.log(describe(new BankTransfer("BG80BNBG96611020345678")));
console.log(describe(new BankTransfer("DE89370400440532013000")));
console.log(describe(new CashOnDelivery()));
Output
card ****4444
transfer from a Bulgarian account
transfer from abroad
cash on delivery
More precisely, if you are curious
  • When destructuring, the variable can have a different name: const { iban: account } = transfer;. You can also do it right in the parameters of a function: ({ iban }) => ....
const transfer = new BankTransfer("BG80BNBG96611020345678");
const { iban: account } = transfer;
console.log(account.slice(0, 2));
const country = ({ iban }) => iban.slice(0, 2);
console.log(country(transfer));
Output
BG
BG

Try it

Which case wins?

The checks go from top to bottom. The first one that is true wins.

What will the program print?

function label(payment, amount) {
if (payment instanceof CashOnDelivery && amount > 500) {
return "cash on delivery: not accepted over €500";
}
if (payment instanceof CashOnDelivery) {
return "cash on delivery";
}
if (payment instanceof CardPayment && amount > 1000) {
return "card: confirmation needed";
}
if (payment instanceof CardPayment) {
return "card";
}
return "transfer";
}
console.log(label(new CashOnDelivery(), 650));
console.log(label(new CashOnDelivery(), 80));
console.log(label(new CardPayment("4111222233334444"), 650));
Answer
More precisely, if you are curious

The check with a condition has to come before the general one for the same kind. The other way around is not an error: the general check catches everything, and the one with the condition is simply never reached:

function label(payment, amount) {
if (payment instanceof CashOnDelivery) {
return "cash on delivery";
}
if (payment instanceof CashOnDelivery && amount > 500) {
return "cash on delivery: not accepted over €500";
}
return "other";
}
console.log(label(new CashOnDelivery(), 650));
Output
cash on delivery

The idea

Text on several lines

Text on several lines, like a receipt or JSON, is hard to read with " and \n on every line. A template literal between two backticks (`) can span several lines:

const receipt = `The Corner Shop
Coffee beans €18.50
Honey €12.00
Total: €30.50`;
console.log(receipt);
const name = "Maria";
const total = 75.5;
const json = `{"customer": "${name}", "total": ${total.toFixed(2)}}`;
console.log(json);
Output
The Corner Shop
Coffee beans €18.50
Honey €12.00
Total: €30.50
{"customer": "Maria", "total": 75.50}
  • The text starts right after the opening backtick. That is why the first line is on the same line.
  • The indentation is kept as it is written. That is why the lines here are all the way to the left.
  • Regular quotes inside are not escaped.
  • ${...} fills in the values, and toFixed(2) gives two digits after the decimal point.

In Java, var lets the compiler infer the type of the variable. In JavaScript, var is the old way to make a variable and is not used in this course. const and let do not write a type either: the type belongs to the value, not the name.

const prices = new Map([
["coffee", 18.5],
["honey", 12],
]);
for (const [name, price] of prices) {
console.log(`${name}: €${price}`);
}
Output
coffee: €18.5
honey: €12

Exercise

Data and cases

Solve the tasks in your editor.

1. Shapes. Three classes: Circle with radius, Rectangle with width and height, and Triangle with base and height. Write area(shape) with instanceof and destructuring, and throw a TypeError for an unknown kind. Also write name(shape), which says "square" for a rectangle with equal sides. Print a circle with radius 1, a 2 by 3 rectangle, a 4 by 4 square, a triangle with base 3 and height 5, and the total area.
Output
circle: 3.14
rectangle: 6.00
square: 16.00
triangle: 7.50
Total: 32.64
2. Delivery price. Three classes: ToOffice with office, ToAddress with city and weightKg, and Pickup with no fields, each with its own toString(). The price: to an office €4.50; to an address €5.90 in Sofia and €6.90 elsewhere, plus €1 for every kilogram over 5; pickup from the store €0. Print each delivery (with its toString()) and its price.
Output
ToOffice(Varna Center) -> €4.50
ToAddress(Sofia, 2) -> €5.90
ToAddress(Varna, 8) -> €9.90
Pickup() -> €0.00
3. A point as a key. The courier passes through points on the map: (2, 3), (5, 1), (2, 3), (0, 0), (5, 1), (2, 3). Make a class Point with x, y and key(), which returns the string (2, 3), and count how many times they were at each point in a Map with (visits.get(key) ?? 0) + 1. At the end, look up the key of new Point(2, 3).
Output
(2, 3): 3
(5, 1): 2
(0, 0): 1
Visits to (2, 3): 3

More tasks

  • A student as a data class. Student with name and grade, whose constructor throws a RangeError for a grade outside 2 to 6 and freezes the object. Sort an array of students with toSorted((a, b) => a.grade - b.grade).
  • Day of the week. Rewrite the "day of the week" task from the lesson "Conditions" as a function with switch and return: the cases from 1 to 5 one under the other, then return "weekday";.
  • A letter. With a multi-line template literal, make a letter confirming a reservation: name, date, number of guests and price in euros.

Test

Check yourself

1. const m = new Map(); m.set(new Seat(5, 12), "Maria"); What does m.get(new Seat(5, 12)) give?
2. What does Object.freeze(this) at the end of the constructor do?
3. What happens in a switch if a case has neither break nor return?
4. What does const { iban } = payment; do?
5. JavaScript has no sealed. How does the function that tells the payment kinds apart protect itself from an unknown kind?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·