diff --git a/exercises/01.classes/01.problem.class-basics/README.mdx b/exercises/01.classes/01.problem.class-basics/README.mdx
index e76e8fa..20e60b0 100644
--- a/exercises/01.classes/01.problem.class-basics/README.mdx
+++ b/exercises/01.classes/01.problem.class-basics/README.mdx
@@ -7,9 +7,37 @@ system.
π¨ Open and:
-1. Create a `Product` class with fields and methods
-2. Create a `ShoppingCart` class that holds products
-3. Instantiate and use these classes
+1. Create and export a `Product` class with:
+ - Public fields: `name` (`string`), `price` (`number`)
+ - A constructor that sets both from parameters `(name, price)`
+ - A `getDescription()` method that returns a string in this exact form:
+ `Product: {name} - ${price}` (include the dollar sign before the price)
+2. Create and export a `ShoppingCart` class with:
+ - A public `items` field typed as `Array`
+ - New carts start with `items` as an empty array
+ - `addItem(product: Product)` that appends the product to `items`
+ - `getTotal()` that returns the sum of every item's `price`
+3. Export both classes: `export { Product, ShoppingCart }`
+
+## Fixtures and success criteria
+
+With these fixtures:
+
+```ts
+const laptop = new Product('Laptop', 999.99)
+const mouse = new Product('Mouse', 29.99)
+const cart = new ShoppingCart()
+cart.addItem(laptop)
+cart.addItem(mouse)
+```
+
+you should observe:
+
+- `laptop.name === 'Laptop'` and `laptop.price === 999.99`
+- `laptop.getDescription() === 'Product: Laptop - $999.99'`
+- A fresh cart starts with `items.length === 0`
+- After the two `addItem` calls above, `items` has length `2` in insertion order
+- `cart.getTotal() === 1029.98`
π° A class includes fields, a constructor, and methods.
diff --git a/exercises/01.classes/01.problem.class-basics/index.ts b/exercises/01.classes/01.problem.class-basics/index.ts
index 1ed717c..1da8eb5 100644
--- a/exercises/01.classes/01.problem.class-basics/index.ts
+++ b/exercises/01.classes/01.problem.class-basics/index.ts
@@ -2,26 +2,29 @@
// π¨ Create a Product class with:
// - Fields: name (string), price (number)
-// - Constructor to initialize both
-// - Method: getDescription() returns "Product: {name} - ${price}"
+// - Constructor(name, price) that initializes both
+// - Method: getDescription() returns exactly:
+// "Product: {name} - ${price}"
+// Example: new Product('Laptop', 999.99).getDescription()
+// β "Product: Laptop - $999.99"
-// Test Product
+// Optional smoke test:
// const laptop = new Product('Laptop', 999.99)
// const mouse = new Product('Mouse', 29.99)
-// console.log(laptop)
-// console.log(mouse)
+// console.log(laptop.getDescription())
+// console.log(mouse.getDescription())
// π¨ Create a ShoppingCart class with:
-// - Field: items (Array)
-// - Constructor to initialize empty items array
-// - Method: addItem(product: Product) adds to items
-// - Method: getTotal() returns sum of all product prices
+// - Field: items (Array), starts empty
+// - Method: addItem(product: Product) appends to items
+// - Method: getTotal() returns the sum of all item prices
+// Example: laptop (999.99) + mouse (29.99) β 1029.98
-// Test ShoppingCart
+// Optional smoke test:
// const cart = new ShoppingCart()
// cart.addItem(laptop)
// cart.addItem(mouse)
-// console.log(cart.getTotal())
-// console.log(cart)
+// console.log(cart.getTotal()) // 1029.98
+// π¨ Export both classes
// export { Product, ShoppingCart }
diff --git a/exercises/01.classes/01.solution.class-basics/index.test.ts b/exercises/01.classes/01.solution.class-basics/index.test.ts
index 2969790..328e8e8 100644
--- a/exercises/01.classes/01.solution.class-basics/index.test.ts
+++ b/exercises/01.classes/01.solution.class-basics/index.test.ts
@@ -35,7 +35,7 @@ await test('Product getDescription should return formatted string', () => {
assert.strictEqual(
sampleLaptop.getDescription(),
'Product: Laptop - $999.99',
- 'π¨ getDescription() should return "Product: Laptop - $999.99" - check your method implementation and formatting',
+ 'π¨ getDescription() should return "Product: Laptop - $999.99"',
)
})
@@ -89,6 +89,6 @@ await test('ShoppingCart getTotal should calculate sum of all item prices', () =
assert.strictEqual(
sampleCart.getTotal(),
1029.98,
- 'π¨ getTotal() should return 1029.98 (sum of 999.99 + 29.99) - check your method implementation and price calculation',
+ 'π¨ getTotal() should return 1029.98 for Laptop (999.99) + Mouse (29.99)',
)
})
diff --git a/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx b/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx
index af78d2f..1788104 100644
--- a/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx
+++ b/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx
@@ -53,11 +53,48 @@ const newCar = new Car('Toyota', 'Camry') // year defaults to 2024
const oldCar = new Car('Ford', 'Mustang', 1965) // year explicitly set
```
-π¨ Open and:
+π¨ Open and create/export three classes:
-1. Use `#` prefix to create private fields
-2. Use default parameter values in constructors
-3. Create classes that encapsulate their internal state
+### `User`
+
+- Public fields: `name`, `email`, `role` (all `string`)
+- Constructor `(name, email, role = 'user')`
+- Omitting `role` must leave it as `'user'`
+
+### `BankAccount`
+
+- Public field: `accountNumber` (`string`)
+- A private balance field (use `#` so it is inaccessible outside the class)
+- New accounts start with balance `0`
+- `deposit(amount: number)` increases the balance by `amount` (it accumulates)
+- `getBalance()` returns the current balance
+
+### `Config`
+
+- Public fields: `host` (`string`), `port` (`number`), `debug` (`boolean`)
+- Constructor defaults: `host = 'localhost'`, `port = 3000`, `debug = false`
+- `new Config()` must use all three defaults; custom args override them
+
+Export all three: `export { User, BankAccount, Config }`
+
+## Fixtures and success criteria
+
+```ts
+const user = new User('Alice', 'alice@example.com')
+const admin = new User('Bob', 'bob@example.com', 'admin')
+const account = new BankAccount('12345')
+account.deposit(100)
+account.deposit(50)
+const config = new Config()
+const customConfig = new Config('example.com', 8080, true)
+```
+
+- `user.role === 'user'` and `admin.role === 'admin'`
+- `account.accountNumber === '12345'`
+- A fresh account has `getBalance() === 0`
+- After the deposits above, `getBalance() === 150`
+- Default config: `host === 'localhost'`, `port === 3000`, `debug === false`
+- Custom config: `'example.com'`, `8080`, `true`
π° Use `#` to declare a private fieldβit's truly private, not just a convention.
diff --git a/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts b/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts
index 1f35ad1..d9e400b 100644
--- a/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts
+++ b/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts
@@ -1,39 +1,39 @@
// Private Fields and Defaults
-// π¨ Create a User class with these fields:
+// π¨ Create a User class with:
// - name: string
// - email: string
-// - role: string (default: 'user')
+// - role: string (default: 'user' when omitted)
// Initialize fields in the constructor
-// Test User
+// Optional smoke test:
// const user = new User('Alice', 'alice@example.com')
// const admin = new User('Bob', 'bob@example.com', 'admin')
-// console.log(user)
-// console.log(admin)
+// console.log(user.role) // 'user'
+// console.log(admin.role) // 'admin'
// π¨ Create a BankAccount class with:
// - accountNumber: string
-// - #balance: number (private field, default: 0)
-// - Method: deposit(amount: number)
-// - Method: getBalance() returns the balance
-// π° #balance is a private field - can only be accessed inside the class
-
-// Test BankAccount
+// - a private balance field using # (starts at 0)
+// - deposit(amount: number) increases the balance by amount
+// - getBalance() returns the current balance
+// π° Private fields can only be read/written inside the class
+//
+// Example:
// const account = new BankAccount('12345')
+// account.getBalance() // 0
// account.deposit(100)
-// console.log(account)
-// console.log(account.getBalance())
-
-// π¨ Create a Config class with optional default values:
-// - host: string (default: 'localhost')
-// - port: number (default: 3000)
-// - debug: boolean (default: false)
+// account.deposit(50)
+// account.getBalance() // 150
-// Test Config
-// const config = new Config()
-// const customConfig = new Config('example.com', 8080, true)
-// console.log(config)
-// console.log(customConfig)
+// π¨ Create a Config class with constructor defaults:
+// - host: string = 'localhost'
+// - port: number = 3000
+// - debug: boolean = false
+//
+// Example:
+// new Config() β localhost / 3000 / false
+// new Config('example.com', 8080, true) β those custom values
+// π¨ Export all three classes
// export { User, BankAccount, Config }
diff --git a/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts b/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts
index a07a64b..22171ea 100644
--- a/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts
+++ b/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts
@@ -84,12 +84,12 @@ await test('BankAccount deposit should increase balance', () => {
assert.strictEqual(
balanceAfterFirstDeposit,
100,
- 'π¨ After depositing 100, getBalance() should return 100 - check your deposit method implementation',
+ 'π¨ After deposit(100), getBalance() should return 100',
)
assert.strictEqual(
sampleAccount.getBalance(),
150,
- 'π¨ After depositing another 50, getBalance() should return 150 - check your deposit method accumulates correctly',
+ 'π¨ After deposit(100) then deposit(50), getBalance() should return 150 (deposits accumulate)',
)
})
diff --git a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx
index 67b5b07..227621c 100644
--- a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx
+++ b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx
@@ -7,10 +7,33 @@ interfaces to ensure all payment methods follow the same contract.
π¨ Open and:
-1. Create a `PaymentMethod` interface with a `pay(amount: number)` method
-2. Create a `CreditCard` class that implements `PaymentMethod`
-3. Create a `PayPal` class that implements `PaymentMethod`
-4. Each class should have its own implementation of `pay()`
+1. Create a `PaymentMethod` interface with:
+ - `pay(amount: number): string`
+2. Create and export a `CreditCard` class that `implements PaymentMethod`:
+ - Public field: `cardNumber` (`string`)
+ - Constructor `(cardNumber: string)`
+ - `pay(amount)` returns exactly:
+ `Paid $${amount} with credit card ${cardNumber}`
+3. Create and export a `PayPal` class that `implements PaymentMethod`:
+ - Public field: `email` (`string`)
+ - Constructor `(email: string)`
+ - `pay(amount)` returns exactly:
+ `Paid $${amount} with PayPal ${email}`
+4. Export the classes: `export { CreditCard, PayPal }`
+ (`PaymentMethod` itself does not need to be exported)
+
+## Fixtures and success criteria
+
+```ts
+const creditCard = new CreditCard('1234-5678-9012-3456')
+const paypal = new PayPal('user@example.com')
+```
+
+- `creditCard.cardNumber === '1234-5678-9012-3456'`
+- `creditCard.pay(100) === 'Paid $100 with credit card 1234-5678-9012-3456'`
+- `paypal.email === 'user@example.com'`
+- `paypal.pay(50) === 'Paid $50 with PayPal user@example.com'`
+- Both instances expose a callable `pay` method
π° Classes that implement an interface must define all its methods.
diff --git a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts
index 7b32ebe..0dd6412 100644
--- a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts
+++ b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts
@@ -1,26 +1,31 @@
// Implementing Interfaces
// π¨ Create a PaymentMethod interface with:
-// - Method: pay(amount: number) returns string
+// - pay(amount: number): string
// π¨ Create a CreditCard class that implements PaymentMethod:
// - Field: cardNumber (string)
-// - Constructor that takes cardNumber
-// - Implement pay(amount: number) returns "Paid $${amount} with credit card ${cardNumber}"
+// - Constructor(cardNumber)
+// - pay(amount) returns exactly:
+// "Paid $${amount} with credit card ${cardNumber}"
+// Example: new CreditCard('1234-5678-9012-3456').pay(100)
+// β "Paid $100 with credit card 1234-5678-9012-3456"
-// Test CreditCard
+// Optional smoke test:
// const creditCard = new CreditCard('1234-5678-9012-3456')
// console.log(creditCard.pay(100))
-// console.log(creditCard)
// π¨ Create a PayPal class that implements PaymentMethod:
// - Field: email (string)
-// - Constructor that takes email
-// - Implement pay(amount: number) returns "Paid $${amount} with PayPal ${email}"
+// - Constructor(email)
+// - pay(amount) returns exactly:
+// "Paid $${amount} with PayPal ${email}"
+// Example: new PayPal('user@example.com').pay(50)
+// β "Paid $50 with PayPal user@example.com"
-// Test PayPal
+// Optional smoke test:
// const paypal = new PayPal('user@example.com')
// console.log(paypal.pay(50))
-// console.log(paypal)
+// π¨ Export the classes (PaymentMethod does not need to be exported)
// export { CreditCard, PayPal }
diff --git a/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts b/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts
index 7536967..160d00e 100644
--- a/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts
+++ b/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts
@@ -26,7 +26,7 @@ await test('CreditCard should implement PaymentMethod interface', () => {
assert.strictEqual(
creditCard.pay(100),
'Paid $100 with credit card 1234-5678-9012-3456',
- 'π¨ pay() should return "Paid $100 with credit card 1234-5678-9012-3456" - check your PaymentMethod interface implementation',
+ 'π¨ CreditCard.pay(100) should return "Paid $100 with credit card 1234-5678-9012-3456"',
)
})
@@ -40,7 +40,7 @@ await test('PayPal should implement PaymentMethod interface', () => {
assert.strictEqual(
paypal.pay(50),
'Paid $50 with PayPal user@example.com',
- 'π¨ pay() should return "Paid $50 with PayPal user@example.com" - check your PaymentMethod interface implementation',
+ 'π¨ PayPal.pay(50) should return "Paid $50 with PayPal user@example.com"',
)
})
diff --git a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx
index e571f9a..56bc954 100644
--- a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx
+++ b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx
@@ -6,11 +6,30 @@
This is called "programming to abstractions"βusing interfaces instead of
concrete classes.
+The `PaymentMethod` interface plus `CreditCard` and `PayPal` are already
+defined in . You only need to add
+`processPayment`.
+
π¨ Open and:
-1. Create a `processPayment` function that accepts a `PaymentMethod` parameter
-2. The function should call `method.pay(amount)` and return the result
-3. Test it with both `CreditCard` and `PayPal` instances
+1. Create a `processPayment` function with this contract:
+ - Parameters: `method: PaymentMethod`, `amount: number`
+ - Return type: `string`
+ - Behavior: return the payment result string for that method and amount
+2. Type the first parameter as `PaymentMethod` (not `CreditCard` or `PayPal`)
+3. Export it with the existing classes:
+ `export { CreditCard, PayPal, processPayment }`
+
+## Fixtures and success criteria
+
+```ts
+const creditCard = new CreditCard('1234-5678-9012-3456')
+const paypal = new PayPal('user@example.com')
+```
+
+- `processPayment(creditCard, 100) === 'Paid $100 with credit card 1234-5678-9012-3456'`
+- `processPayment(paypal, 50) === 'Paid $50 with PayPal user@example.com'`
+- The same function works for both implementations without special-casing
π° Use the interface type (`PaymentMethod`) for the parameter instead of a
concrete class type. This way, the function works with any class that
diff --git a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts
index 4098bac..9292080 100644
--- a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts
+++ b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts
@@ -29,11 +29,17 @@ class PayPal implements PaymentMethod {
}
// π¨ Create a processPayment function:
-// - Parameters: method (PaymentMethod), amount (number)
-// - Returns: string
-// - Calls method.pay(amount)
-
-// Test processPayment
+// - Parameters: method: PaymentMethod, amount: number
+// - Returns: the payment result string for that method and amount
+// - Type the first parameter as PaymentMethod, not a concrete class
+//
+// Expected results (using the classes above):
+// processPayment(new CreditCard('1234-5678-9012-3456'), 100)
+// β "Paid $100 with credit card 1234-5678-9012-3456"
+// processPayment(new PayPal('user@example.com'), 50)
+// β "Paid $50 with PayPal user@example.com"
+
+// Optional smoke test:
// const creditCard = new CreditCard('1234-5678-9012-3456')
// const paypal = new PayPal('user@example.com')
// console.log(processPayment(creditCard, 100))
diff --git a/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx b/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx
index e046970..5a56071 100644
--- a/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx
+++ b/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx
@@ -7,10 +7,27 @@ it with specific shapes like `Circle` and `Rectangle`.
π¨ Open and:
-1. Create a `Shape` base class with a `color` field
-2. Create a `Circle` class that extends `Shape`
-3. Add a `radius` field to `Circle`
-4. Create a `Rectangle` class that extends `Shape`
-5. Add `width` and `height` fields to `Rectangle`
+1. Create a `Shape` base class with:
+ - Public field: `color` (`string`)
+ - Constructor `(color: string)`
+2. Create a `Circle` class that `extends Shape` with:
+ - Public field: `radius` (`number`)
+ - Constructor `(color, radius)` that initializes the parent via `super(color)`
+3. Create a `Rectangle` class that `extends Shape` with:
+ - Public fields: `width` (`number`), `height` (`number`)
+ - Constructor `(color, width, height)` that calls `super(color)`
+4. Export all three: `export { Shape, Circle, Rectangle }`
+
+## Fixtures and success criteria
+
+```ts
+const circle = new Circle('red', 5)
+const rectangle = new Rectangle('blue', 10, 20)
+```
+
+- `circle.color === 'red'` and `circle.radius === 5`
+- `rectangle.color === 'blue'`, `width === 10`, `height === 20`
+- `circle instanceof Shape` and `rectangle instanceof Shape` are both `true`
+ (each subclass is substitutable as a `Shape`)
π [TypeScript Handbook - Inheritance](https://www.typescriptlang.org/docs/handbook/2/classes.html#inheritance)
diff --git a/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts b/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts
index 43bd8f6..7565e5e 100644
--- a/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts
+++ b/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts
@@ -2,28 +2,30 @@
// π¨ Create a Shape base class with:
// - Field: color (string)
-// - Constructor that takes color
+// - Constructor(color)
// π¨ Create a Circle class that extends Shape:
// - Field: radius (number)
-// - Constructor that takes color and radius
-// - Call super(color) to initialize parent
+// - Constructor(color, radius) that calls super(color)
+//
+// Example: new Circle('red', 5)
+// β color 'red' (from Shape), radius 5
+// β instanceof Shape === true
-// Test Circle
+// Optional smoke test:
// const circle = new Circle('red', 5)
-// console.log(circle.color) // β
Should work - inherited from Shape
-// console.log(circle.radius) // β
Should work - defined in Circle
-// console.log(circle)
+// console.log(circle.color, circle.radius)
// π¨ Create a Rectangle class that extends Shape:
// - Fields: width (number), height (number)
-// - Constructor that takes color, width, and height
-// - Call super(color) to initialize parent
+// - Constructor(color, width, height) that calls super(color)
+//
+// Example: new Rectangle('blue', 10, 20)
+// β color 'blue', width 10, height 20
-// Test Rectangle
+// Optional smoke test:
// const rectangle = new Rectangle('blue', 10, 20)
-// console.log(rectangle.color) // β
Should work - inherited from Shape
-// console.log(rectangle.width, rectangle.height) // β
Should work
-// console.log(rectangle)
+// console.log(rectangle.color, rectangle.width, rectangle.height)
+// π¨ Export all three classes
// export { Shape, Circle, Rectangle }
diff --git a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx
index d948af5..dda3d8f 100644
--- a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx
+++ b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx
@@ -10,8 +10,24 @@ already defined. You'll add and override the `getArea()` method.
π¨ Open and:
-1. Add a `getArea()` method to `Shape` that returns 0
-2. Override `getArea()` in `Circle` to return Ο Γ radiusΒ²
-3. Override `getArea()` in `Rectangle` to return width Γ height
+1. Add `getArea(): number` on `Shape` that returns `0`
+2. Override `getArea()` on `Circle` so a circle's area is Ο Γ radiusΒ²
+ (use `Math.PI`)
+3. Override `getArea()` on `Rectangle` so a rectangle's area is width Γ height
+4. Keep exporting: `export { Shape, Circle, Rectangle }`
+
+## Fixtures and success criteria
+
+```ts
+const shape = new Shape('red')
+const circle = new Circle('red', 5)
+const rectangle = new Rectangle('blue', 10, 20)
+```
+
+- `shape.getArea() === 0`
+- `circle.getArea()` is about `78.54` (within `0.01` of `Math.PI * 25`)
+- `rectangle.getArea() === 200`
+- Circle and rectangle areas are both greater than `0` and not equal to each
+ other
π [TypeScript Handbook - Overriding Methods](https://www.typescriptlang.org/docs/handbook/2/classes.html#overriding-methods)
diff --git a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts
index 26ae848..1077081 100644
--- a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts
+++ b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts
@@ -7,7 +7,7 @@ class Shape {
this.color = color
}
- // π¨ Add a getArea() method that returns 0
+ // π¨ Add getArea(): number that returns 0
}
class Circle extends Shape {
@@ -18,7 +18,8 @@ class Circle extends Shape {
this.radius = radius
}
- // π¨ Override getArea() to return Math.PI * radius ** 2
+ // π¨ Override getArea() so area is Ο Γ radiusΒ² (use Math.PI)
+ // Example: new Circle('red', 5).getArea() β 78.54
}
class Rectangle extends Shape {
@@ -31,13 +32,15 @@ class Rectangle extends Shape {
this.height = height
}
- // π¨ Override getArea() to return width * height
+ // π¨ Override getArea() so area is width Γ height
+ // Example: new Rectangle('blue', 10, 20).getArea() === 200
}
-// Test getArea methods
+// Optional smoke test:
// const circle = new Circle('red', 5)
// const rectangle = new Rectangle('blue', 10, 20)
-// console.log(circle.getArea()) // Should print ~78.54
-// console.log(rectangle.getArea()) // Should print 200
+// console.log(circle.getArea()) // ~78.54
+// console.log(rectangle.getArea()) // 200
+// π¨ Keep exporting the classes
// export { Shape, Circle, Rectangle }
diff --git a/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts b/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts
index f4aaca4..a758ac3 100644
--- a/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts
+++ b/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts
@@ -28,7 +28,7 @@ await test('Shape getArea should return 0', () => {
assert.strictEqual(
baseShape.getArea(),
0,
- 'π¨ Shape.getArea() should return 0 - check your base class method implementation',
+ 'π¨ Shape.getArea() should return 0',
)
})
@@ -37,11 +37,11 @@ await test('Circle should override getArea to calculate circle area', () => {
const circleArea = sampleCircle.getArea()
assert.ok(
Math.abs(circleArea - Math.PI * 25) < 0.01,
- 'π¨ Circle.getArea() should be approximately Math.PI * 25 - check that you override getArea() with the circle area formula',
+ 'π¨ Circle.getArea() for radius 5 should be within 0.01 of Math.PI * 25',
)
assert.ok(
Math.abs(circleArea - 78.54) < 0.01,
- 'π¨ Circle.getArea() should be approximately 78.54 - check your circle area calculation (Ο * radiusΒ²)',
+ 'π¨ Circle.getArea() for radius 5 should be about 78.54',
)
})
@@ -51,7 +51,7 @@ await test('Rectangle should override getArea to calculate rectangle area', () =
assert.strictEqual(
rectangleArea,
200,
- 'π¨ Rectangle.getArea() should return 200 (10 * 20) - check that you override getArea() with the rectangle area formula',
+ 'π¨ Rectangle.getArea() for 10Γ20 should return 200',
)
})
@@ -61,17 +61,14 @@ await test('Different shapes should have different area calculations', () => {
const circleArea = sampleCircle.getArea()
const rectangleArea = sampleRectangle.getArea()
- assert.ok(
- circleArea > 0,
- 'π¨ Circle area should be greater than 0 - check your getArea() override implementation',
- )
+ assert.ok(circleArea > 0, 'π¨ Circle.getArea() should be greater than 0')
assert.ok(
rectangleArea > 0,
- 'π¨ Rectangle area should be greater than 0 - check your getArea() override implementation',
+ 'π¨ Rectangle.getArea() should be greater than 0',
)
assert.notStrictEqual(
circleArea,
rectangleArea,
- 'π¨ Circle and Rectangle areas should be different - check that each class overrides getArea() with its own calculation',
+ 'π¨ Circle and Rectangle getArea() results should differ for these fixtures',
)
})
diff --git a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx
index fd1b419..3cfff77 100644
--- a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx
+++ b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx
@@ -5,15 +5,36 @@
π¨βπΌ Let's build a media player that can play different types of media files.
We'll use polymorphism to handle all media types uniformly.
-The `MediaFile`, `AudioFile`, and `VideoFile` classes are already defined.
-You'll create a `MediaPlayer` that works with any of them.
+The `MediaFile`, `AudioFile`, and `VideoFile` classes are already defined with
+these `play()` results:
+
+- `new MediaFile('file.mp3').play()` β `'Playing file.mp3'`
+- `new AudioFile('song.mp3').play()` β `'Playing audio: song.mp3'`
+- `new VideoFile('movie.mp4').play()` β `'Playing video: movie.mp4'`
+
+You'll create a `MediaPlayer` that works with any of them through the base type.
π¨ Open and:
-1. Create a `MediaPlayer` class with a `playFile()` method
-2. The method should accept a `MediaFile` parameter
-3. Call `media.play()` and return the result
-4. Test it with both `AudioFile` and `VideoFile` instances
+1. Create a `MediaPlayer` class with `playFile(media: MediaFile): string`
+2. `playFile` must accept the **base** `MediaFile` type (not only subclasses)
+3. Return that media file's play result string
+4. Export everything:
+ `export { MediaFile, AudioFile, VideoFile, MediaPlayer }`
+
+## Fixtures and success criteria
+
+```ts
+const player = new MediaPlayer()
+```
+
+- `player.playFile(new MediaFile('file.mp3')) === 'Playing file.mp3'`
+- `player.playFile(new AudioFile('song.mp3')) === 'Playing audio: song.mp3'`
+- `player.playFile(new VideoFile('movie.mp4')) === 'Playing video: movie.mp4'`
+
+Because the parameter is typed as `MediaFile`, `AudioFile` and `VideoFile`
+instances are valid arguments (substitutability). You should not need separate
+overloads or type checks for each subclass.
π° The key insight: `playFile()` accepts `MediaFile`, but it works with any
subclass (`AudioFile`, `VideoFile`) because of substitutabilityβsubclasses
diff --git a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts
index a94869d..622ab5c 100644
--- a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts
+++ b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts
@@ -25,14 +25,19 @@ class VideoFile extends MediaFile {
}
// π¨ Create a MediaPlayer class with:
-// - Method: playFile(media: MediaFile) returns string
-// - Calls media.play() and returns the result
-
-// Test MediaPlayer (polymorphism)
-// const audio = new AudioFile('song.mp3')
-// const video = new VideoFile('movie.mp4')
+// - playFile(media: MediaFile): string
+// - Parameter type must be MediaFile (the base class)
+// - Returns that media file's play result string
+//
+// Expected results:
+// playFile(new MediaFile('file.mp3')) β "Playing file.mp3"
+// playFile(new AudioFile('song.mp3')) β "Playing audio: song.mp3"
+// playFile(new VideoFile('movie.mp4')) β "Playing video: movie.mp4"
+
+// Optional smoke test:
// const player = new MediaPlayer()
-// console.log(player.playFile(audio)) // Should work - AudioFile is substitutable for MediaFile
-// console.log(player.playFile(video)) // Should work - VideoFile is substitutable for MediaFile
+// console.log(player.playFile(new AudioFile('song.mp3')))
+// console.log(player.playFile(new VideoFile('movie.mp4')))
+// π¨ Export MediaFile, AudioFile, VideoFile, and MediaPlayer
// export { MediaFile, AudioFile, VideoFile, MediaPlayer }
diff --git a/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts b/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts
index 1e2d4b5..beab766 100644
--- a/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts
+++ b/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts
@@ -36,7 +36,7 @@ await test('MediaPlayer should accept MediaFile instances', () => {
assert.strictEqual(
basePlayer.playFile(base),
'Playing file.mp3',
- 'π¨ playFile() should return "Playing file.mp3" - check your MediaPlayer.playFile method accepts MediaFile type',
+ 'π¨ playFile(new MediaFile("file.mp3")) should return "Playing file.mp3"',
)
})
@@ -46,7 +46,7 @@ await test('MediaPlayer should accept AudioFile instances (polymorphism)', () =>
assert.strictEqual(
basePlayer.playFile(audio),
'Playing audio: song.mp3',
- 'π¨ playFile() should return "Playing audio: song.mp3" - check that AudioFile extends MediaFile and playFile accepts the base type',
+ 'π¨ playFile(new AudioFile("song.mp3")) should return "Playing audio: song.mp3" (AudioFile must be accepted as MediaFile)',
)
})
@@ -56,7 +56,7 @@ await test('MediaPlayer should accept VideoFile instances (polymorphism)', () =>
assert.strictEqual(
basePlayer.playFile(video),
'Playing video: movie.mp4',
- 'π¨ playFile() should return "Playing video: movie.mp4" - check that VideoFile extends MediaFile and playFile accepts the base type',
+ 'π¨ playFile(new VideoFile("movie.mp4")) should return "Playing video: movie.mp4" (VideoFile must be accepted as MediaFile)',
)
})
diff --git a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx
index 6319e9c..3ac7161 100644
--- a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx
+++ b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx
@@ -7,11 +7,44 @@ inheritance or composition for different scenarios.
π¨ Open and:
-1. Create a `Logger` class with a `log(message: string)` method
-2. Create an `EmailService` class that uses composition (has-a Logger)
-3. Create a `FileLogger` class that uses inheritance (is-a Logger)
-4. Create a `ConsoleLogger` class that uses inheritance (is-a Logger)
-5. Demonstrate when each approach is appropriate
+### Inheritance (`is-a`)
+
+1. Create a `Logger` base class with `log(message: string): void` that prints
+ exactly `Log: {message}`
+2. Create a `FileLogger` that `extends Logger` and overrides `log` to print
+ exactly `File Log: {message}`
+3. Create a `ConsoleLogger` that `extends Logger` and overrides `log` to print
+ exactly `Console Log: {message}`
+
+### Composition (`has-a`)
+
+4. Create an `EmailService` that **has** a logger (composition), not is a logger:
+ - Accept a `Logger` in the constructor
+ - Keep that logger as private class state (use `#`)
+ - `sendEmail(to: string, subject: string): void` must produce the send-action
+ message `Sending email to ${to}: ${subject}` **through the injected logger
+ and on the console** (both)
+5. Export all four:
+ `export { Logger, FileLogger, ConsoleLogger, EmailService }`
+
+## Fixtures and success criteria
+
+```ts
+const fileLogger = new FileLogger()
+const consoleLogger = new ConsoleLogger()
+const emailService = new EmailService(fileLogger)
+emailService.sendEmail('user@example.com', 'Welcome')
+```
+
+- `fileLogger instanceof Logger` and `consoleLogger instanceof Logger`
+- `fileLogger.log('hi')` prints `File Log: hi`
+- `consoleLogger.log('hi')` prints `Console Log: hi`
+- `new Logger().log('hi')` prints `Log: hi`
+- `EmailService` can be constructed with either logger subclass
+- After `sendEmail('user@example.com', 'Welcome')` with a `FileLogger`, console
+ output includes both:
+ - `File Log: Sending email to user@example.com: Welcome`
+ - `Sending email to user@example.com: Welcome`
π° Think about the relationship: Is it "is-a" or "has-a"?
diff --git a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts
index 8f13d84..e123322 100644
--- a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts
+++ b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts
@@ -1,37 +1,35 @@
// When to Use Composition vs Inheritance
// π¨ Create a Logger base class with:
-// - Method: log(message: string) returns void
-// - Prints "Log: {message}" to console
+// - log(message: string): void
+// - Prints exactly "Log: {message}"
// π¨ Create a FileLogger class using INHERITANCE (is-a):
// - Extends Logger
-// - Override log() to print "File Log: {message}"
+// - Override log() to print exactly "File Log: {message}"
-// Test FileLogger
+// Optional smoke test:
// const fileLogger = new FileLogger()
-// fileLogger.log('File system initialized')
-// console.log(fileLogger)
+// fileLogger.log('File system initialized') // File Log: File system initialized
// π¨ Create a ConsoleLogger class using INHERITANCE (is-a):
// - Extends Logger
-// - Override log() to print "Console Log: {message}"
+// - Override log() to print exactly "Console Log: {message}"
-// Test ConsoleLogger
+// Optional smoke test:
// const consoleLogger = new ConsoleLogger()
-// consoleLogger.log('Console ready')
-// console.log(consoleLogger)
+// consoleLogger.log('Console ready') // Console Log: Console ready
// π¨ Create an EmailService class using COMPOSITION (has-a):
-// - Field: #logger (Logger) - private field
-// - Constructor that takes a Logger
-// - Method: sendEmail(to: string, subject: string) returns void
-// - Uses this.#logger.log() to log the email action
-// - Prints "Sending email to {to}: {subject}"
-
-// Test EmailService
-// const emailService = new EmailService(fileLogger)
-// emailService.sendEmail('user@example.com', 'Welcome')
-// console.log(emailService)
-
+// - Constructor takes a Logger and stores it privately (use #)
+// - sendEmail(to, subject): void
+// - Must produce "Sending email to {to}: {subject}" through the injected
+// logger AND on the console (both)
+//
+// With a FileLogger, sendEmail('user@example.com', 'Welcome') should result in
+// console output that includes:
+// File Log: Sending email to user@example.com: Welcome
+// Sending email to user@example.com: Welcome
+
+// π¨ Export all four classes
// export { Logger, FileLogger, ConsoleLogger, EmailService }
diff --git a/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts b/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts
index 3d706ca..eb7ff01 100644
--- a/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts
+++ b/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts
@@ -2,6 +2,20 @@ import assert from 'node:assert/strict'
import { test } from 'node:test'
import * as solution from './index.ts'
+function captureConsoleLog(run: () => void): Array {
+ const lines: Array = []
+ const original = console.log
+ console.log = (...args: Array) => {
+ lines.push(args.map(String).join(' '))
+ }
+ try {
+ run()
+ } finally {
+ console.log = original
+ }
+ return lines
+}
+
await test('Logger class should be exported', () => {
assert.ok(
'Logger' in solution,
@@ -46,6 +60,31 @@ await test('ConsoleLogger should extend Logger (inheritance)', () => {
)
})
+await test('Logger log formats should match the required prefixes', () => {
+ const baseLines = captureConsoleLog(() => {
+ new solution.Logger().log('hello')
+ })
+ const fileLines = captureConsoleLog(() => {
+ new solution.FileLogger().log('hello')
+ })
+ const consoleLines = captureConsoleLog(() => {
+ new solution.ConsoleLogger().log('hello')
+ })
+
+ assert.ok(
+ baseLines.includes('Log: hello'),
+ 'π¨ Logger.log("hello") should print "Log: hello"',
+ )
+ assert.ok(
+ fileLines.includes('File Log: hello'),
+ 'π¨ FileLogger.log("hello") should print "File Log: hello"',
+ )
+ assert.ok(
+ consoleLines.includes('Console Log: hello'),
+ 'π¨ ConsoleLogger.log("hello") should print "Console Log: hello"',
+ )
+})
+
await test('EmailService should accept any Logger (composition)', () => {
const fileLogger = new solution.FileLogger()
const consoleLogger = new solution.ConsoleLogger()
@@ -62,13 +101,26 @@ await test('EmailService should accept any Logger (composition)', () => {
)
})
-await test('EmailService should use logger in sendEmail method', () => {
+await test('EmailService sendEmail should use the injected logger and print the send action', () => {
const fileLogger = new solution.FileLogger()
- const emailService1 = new solution.EmailService(fileLogger)
+ const emailService = new solution.EmailService(fileLogger)
assert.ok(
- typeof emailService1.sendEmail === 'function',
+ typeof emailService.sendEmail === 'function',
'π¨ EmailService.sendEmail should be a function - check your method definition',
)
+
+ const lines = captureConsoleLog(() => {
+ emailService.sendEmail('user@example.com', 'Welcome')
+ })
+
+ assert.ok(
+ lines.includes('File Log: Sending email to user@example.com: Welcome'),
+ 'π¨ After sendEmail("user@example.com", "Welcome") with FileLogger, output should include "File Log: Sending email to user@example.com: Welcome"',
+ )
+ assert.ok(
+ lines.includes('Sending email to user@example.com: Welcome'),
+ 'π¨ After sendEmail("user@example.com", "Welcome"), output should also include "Sending email to user@example.com: Welcome"',
+ )
})
await test('EmailService should work with different logger types', () => {
@@ -85,4 +137,14 @@ await test('EmailService should work with different logger types', () => {
emailService2 instanceof solution.EmailService,
'π¨ emailService2 should be an instance of EmailService - check your class definition',
)
+
+ const consoleLines = captureConsoleLog(() => {
+ emailService2.sendEmail('admin@example.com', 'Alert')
+ })
+ assert.ok(
+ consoleLines.includes(
+ 'Console Log: Sending email to admin@example.com: Alert',
+ ),
+ 'π¨ After sendEmail with ConsoleLogger, output should include "Console Log: Sending email to admin@example.com: Alert"',
+ )
})
diff --git a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx
index bc886b8..5619d97 100644
--- a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx
+++ b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx
@@ -6,12 +6,44 @@
implementations without changing `EmailService`. This is dependency injectionβa
key benefit of composition.
+`Logger` and `EmailService` from the previous step are already provided.
+`EmailService.sendEmail(to, subject)` produces
+`Sending email to ${to}: ${subject}` through whatever logger you inject (and
+also on the console).
+
π¨ Open and:
-1. Create a `MockLogger` class that extends `Logger` and stores logs in memory (useful for testing)
-2. Create a `SilentLogger` class that extends `Logger` and does nothing (useful for production)
-3. Both should extend the `Logger` class from step 01
-4. Demonstrate that `EmailService` works with any logger implementation
+1. Create and export a `MockLogger` that `extends Logger`:
+ - Override `log(message)` to **store** each message in order (do not print)
+ - Expose `getLogs(): Array` that returns the stored messages
+2. Create and export a `SilentLogger` that `extends Logger`:
+ - Override `log(message)` to do nothing (no print, no storage, no throw)
+3. Demonstrate that the existing `EmailService` works with both loggers
+4. Export:
+ `export { Logger, EmailService, MockLogger, SilentLogger }`
+
+## Fixtures and success criteria
+
+```ts
+const mockLogger = new MockLogger()
+mockLogger.log('Test message 1')
+mockLogger.log('Test message 2')
+
+const emailService = new EmailService(mockLogger)
+emailService.sendEmail('test@example.com', 'Test Subject')
+
+const silentLogger = new SilentLogger()
+const silentEmail = new EmailService(silentLogger)
+silentEmail.sendEmail('user@example.com', 'Welcome')
+```
+
+- `mockLogger.getLogs()` starts as `['Test message 1', 'Test message 2']` before
+ `sendEmail`
+- After `sendEmail('test@example.com', 'Test Subject')`, `getLogs()` includes
+ `'Sending email to test@example.com: Test Subject'`
+- `silentLogger instanceof Logger` is `true`
+- Using `SilentLogger` with `EmailService` does not throw
+- The same `EmailService` API works unchanged with either logger
π° This is the power of dependency injection: `EmailService` doesn't care which
logger it getsβit just uses whatever logger is provided. This makes testing easy
diff --git a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts
index 72109b8..7468721 100644
--- a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts
+++ b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts
@@ -22,31 +22,18 @@ class EmailService {
}
// π¨ Create a MockLogger class that extends Logger:
-// - Extends Logger
-// - Override log() to store messages in an array: #logs (private field)
-// - Stores messages instead of printing them
-// - Method: getLogs() returns Array to retrieve stored messages
-
-// Test MockLogger
-// const mockLogger = new MockLogger()
-// mockLogger.log('Test message')
-// console.log(mockLogger.getLogs()) // Should show ['Test message']
-
-// Test EmailService with MockLogger (dependency injection)
-// const mockLogger = new MockLogger()
-// const emailService = new EmailService(mockLogger)
-// emailService.sendEmail('test@example.com', 'Test Subject')
-// console.log(mockLogger.getLogs()) // Should show the email log
-// console.log(emailService)
+// - Override log(message) to store messages in order (do not print)
+// - getLogs(): Array returns the stored messages
+//
+// Expected:
+// mockLogger.log('Test message 1'); mockLogger.log('Test message 2')
+// β getLogs() is ['Test message 1', 'Test message 2']
+// After EmailService(mockLogger).sendEmail('test@example.com', 'Test Subject')
+// β getLogs() includes 'Sending email to test@example.com: Test Subject'
// π¨ Create a SilentLogger class that extends Logger:
-// - Extends Logger
-// - Override log() to do nothing (silent logging for production)
-
-// Test SilentLogger
-// const silentLogger = new SilentLogger()
-// const emailService2 = new EmailService(silentLogger)
-// emailService2.sendEmail('user@example.com', 'Welcome')
-// console.log(emailService2)
+// - Override log(message) to do nothing (no print / no throw)
+// - EmailService + SilentLogger should not throw on sendEmail
+// π¨ Export Logger, EmailService, MockLogger, and SilentLogger
// export { Logger, EmailService, MockLogger, SilentLogger }
diff --git a/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts b/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts
index d5060c1..74d8954 100644
--- a/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts
+++ b/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts
@@ -36,6 +36,11 @@ await test('MockLogger should store logs', () => {
'Test message 1',
'π¨ First log should be "Test message 1" - check your log storage',
)
+ assert.strictEqual(
+ logs[1],
+ 'Test message 2',
+ 'π¨ Second log should be "Test message 2" - check that log() stores messages in order',
+ )
})
await test('MockLogger should work with EmailService (dependency injection)', () => {
@@ -45,12 +50,8 @@ await test('MockLogger should work with EmailService (dependency injection)', ()
const logs = mockLogger.getLogs()
assert.ok(
- logs.length > 0,
- 'π¨ MockLogger should have logs after sendEmail - check that EmailService uses the logger',
- )
- assert.ok(
- logs.some((log) => log.includes('test@example.com')),
- 'π¨ Logs should contain email address - check that EmailService logs the email action',
+ logs.includes('Sending email to test@example.com: Test Subject'),
+ 'π¨ After sendEmail("test@example.com", "Test Subject"), getLogs() should include "Sending email to test@example.com: Test Subject"',
)
})
@@ -93,7 +94,7 @@ await test('EmailService should work with different logger implementations', ()
const logs = mockLogger.getLogs()
assert.ok(
- logs.length > 0,
- 'π¨ MockLogger should capture logs from EmailService - demonstrating dependency injection',
+ logs.includes('Sending email to test1@example.com: Test 1'),
+ 'π¨ MockLogger should capture "Sending email to test1@example.com: Test 1" from EmailService',
)
})
diff --git a/extra/01.practice-everything/README.mdx b/extra/01.practice-everything/README.mdx
index 891dd87..94dac66 100644
--- a/extra/01.practice-everything/README.mdx
+++ b/extra/01.practice-everything/README.mdx
@@ -1,3 +1,58 @@
# Practice Everything
-π¨ Open and work through each section.
+π¨ Open and work through each section. There are
+no automated tests hereβuse the examples below as your success criteria.
+
+## Section 1: Class Basics
+
+- `Product.getDescription()` returns `{name}: ${price}`
+ (example: `Mug: $12`)
+- `ShoppingCart` starts empty; `addItem` appends; `getTotal` sums prices
+- Example: Mug `12` + Notebook `8` β total `20`
+
+## Section 2: Private Fields & Defaults
+
+- `BankAccount` starts at balance `0` when constructed with no args
+- `deposit` / `withdraw` change balance; `getBalance` returns it
+- Example: deposit `50`, withdraw `10` β balance `40`
+- `Config` defaults: `host = 'localhost'`, `port = 3000`, `role = 'user'`
+
+## Section 3: Interfaces
+
+- `CreditCard.pay(25)` β `Paid $25 with card 1234` (for card `'1234'`)
+- `PayPal.pay(25)` β `Paid $25 with PayPal user@example.com`
+
+## Section 4: Programming to Abstractions
+
+- `processPayment(method, amount)` returns that method's payment result for the
+ amount
+- `GiftCard` with code `'GC-001'`: `pay(40)` β
+ `Paid $40 with gift card GC-001`
+
+## Section 5: Inheritance
+
+- `Package.getLabel()` β `{label} ({weight}kg)`
+- Example: `new Box('Box A', 5, 10).getLabel()` β `Box A (5kg)`
+- Example: `new Crate('Crate B', 20, 'wood').getLabel()` β `Crate B (20kg)`
+
+## Section 6: Method Overriding
+
+- `Shape.getArea()` β `0`
+- `new Circle(2).getArea()` is about `12.57` (Ο Γ radiusΒ²)
+- `new Rectangle(3, 4).getArea()` β `12`
+
+## Section 7: Substitutability
+
+- `MediaPlayer.playFile` accepts `MediaFile` and returns that file's play result
+- Audio example: `Playing audio song.mp3`
+- Video example: `Playing video movie.mp4`
+
+## Section 8: Composition & Dependency Injection
+
+- `Logger.log(message)` **returns** the prefixed string `Log: {message}`
+- `InMemoryLogger` **stores the raw `message` argument** (not the prefixed
+ return value); `getLogs()` returns a copy of those stored messages
+- `ReportService.generateReport('Weekly Summary')` calls the injected logger
+ with `Report: Weekly Summary`
+- After that call with an `InMemoryLogger`, `getLogs()` includes
+ `Report: Weekly Summary` (the argument that was logged)
diff --git a/extra/01.practice-everything/index.ts b/extra/01.practice-everything/index.ts
index ce1a503..fd08317 100644
--- a/extra/01.practice-everything/index.ts
+++ b/extra/01.practice-everything/index.ts
@@ -9,13 +9,14 @@
// - public fields: name (string), price (number)
// - a constructor that takes name and price
// - a method `getDescription()` that returns "{name}: ${price}"
+// Example: new Product('Mug', 12).getDescription() β "Mug: $12"
// π¨ Create a class `ShoppingCart` with:
// - a public field `items` initialized to an empty array of Product
// - a method `addItem(item: Product)` that adds to items
// - a method `getTotal()` that returns the total price of all items
-// Test Section 1:
+// Optional smoke test:
// const cart = new ShoppingCart()
// cart.addItem(new Product('Mug', 12))
// cart.addItem(new Product('Notebook', 8))
@@ -26,23 +27,21 @@
// ============================================================================
// π¨ Create a class `BankAccount` with:
-// - a private field `#balance` (number)
-// - a constructor that takes an initial balance (default 0)
-// - a method `deposit(amount: number)` that adds to balance
-// - a method `withdraw(amount: number)` that subtracts from balance
-// - a method `getBalance()` that returns the balance
+// - a private balance field (use #; default 0)
+// - deposit(amount) / withdraw(amount) / getBalance()
+// Example: deposit(50), withdraw(10) β getBalance() === 40
// π¨ Create a class `Config` with:
// - public fields: host (string), port (number), role (string)
-// - default values: host = 'localhost', port = 3000, role = 'user'
+// - defaults: host = 'localhost', port = 3000, role = 'user'
-// Test Section 2:
+// Optional smoke test:
// const account = new BankAccount()
// account.deposit(50)
// account.withdraw(10)
// console.log(account.getBalance()) // 40
// const config = new Config()
-// console.log(config.host, config.port, config.role)
+// console.log(config.host, config.port, config.role) // localhost 3000 user
// ============================================================================
// SECTION 3: Interfaces & Implementations
@@ -54,12 +53,15 @@
// π¨ Create a class `CreditCard` that implements PaymentMethod
// - public field: cardNumber (string)
// - pay returns "Paid $${amount} with card ${cardNumber}"
+// Example: new CreditCard('1234').pay(25) β "Paid $25 with card 1234"
// π¨ Create a class `PayPal` that implements PaymentMethod
// - public field: email (string)
// - pay returns "Paid $${amount} with PayPal ${email}"
+// Example: new PayPal('user@example.com').pay(25)
+// β "Paid $25 with PayPal user@example.com"
-// Test Section 3:
+// Optional smoke test:
// const card = new CreditCard('1234')
// const paypal = new PayPal('user@example.com')
// console.log(card.pay(25))
@@ -71,7 +73,7 @@
// π¨ Create a function `processPayment` that:
// - takes a PaymentMethod and an amount
-// - returns the result of calling pay on the method
+// - returns that method's payment result for the amount
// π¨ Create a class `GiftCard` that implements PaymentMethod
// - public field: code (string)
@@ -112,13 +114,15 @@
// π¨ Create a class `Circle` that extends Shape
// - field: radius (number)
-// - override getArea to return Math.PI * radius * radius
+// - override getArea for a circle (Ο Γ radiusΒ²; use Math.PI)
+// Example: new Circle(2).getArea() β 12.57
// π¨ Create a class `Rectangle` that extends Shape
// - fields: width (number), height (number)
-// - override getArea to return width * height
+// - override getArea for a rectangle
+// Example: new Rectangle(3, 4).getArea() === 12
-// Test Section 6:
+// Optional smoke test:
// console.log(new Circle(2).getArea())
// console.log(new Rectangle(3, 4).getArea())
@@ -132,14 +136,17 @@
// π¨ Create a class `AudioFile` that extends MediaFile
// - override play to return "Playing audio {filename}"
+// Example: "Playing audio song.mp3"
// π¨ Create a class `VideoFile` that extends MediaFile
// - override play to return "Playing video {filename}"
+// Example: "Playing video movie.mp4"
// π¨ Create a class `MediaPlayer` with:
-// - a method `playFile(file: MediaFile)` that returns file.play()
+// - playFile(file: MediaFile) returns that file's play result string
+// (parameter type must be the base MediaFile)
-// Test Section 7:
+// Optional smoke test:
// const player = new MediaPlayer()
// console.log(player.playFile(new AudioFile('song.mp3')))
// console.log(player.playFile(new VideoFile('movie.mp4')))
@@ -149,20 +156,26 @@
// ============================================================================
// π¨ Create a class `Logger` with:
-// - a method `log(message: string)` that returns `Log: {message}`
+// - log(message: string) returns the prefixed string `Log: {message}`
// π¨ Create a class `ConsoleLogger` that extends Logger
-// - override log to call console.log and return the same string
+// - override log to print and return that same prefixed string
// π¨ Create a class `InMemoryLogger` that extends Logger
-// - a private field `#logs` (array of strings)
-// - override log to store messages in #logs
-// - a method `getLogs()` that returns a copy of #logs
+// - store the raw message argument privately (not the prefixed return value)
+// - still return the prefixed `Log: {message}` string from log()
+// - getLogs() returns a copy of the stored raw messages
// π¨ Create a class `ReportService` that takes a Logger in its constructor
-// - method `generateReport(title: string)` that logs "Report: {title}"
+// - generateReport(title) logs "Report: {title}" through the injected logger
+//
+// Example:
+// const logger = new InMemoryLogger()
+// new ReportService(logger).generateReport('Weekly Summary')
+// logger.getLogs() includes "Report: Weekly Summary"
+// (the argument passed to log β not "Log: Report: Weekly Summary")
-// Test Section 8:
+// Optional smoke test:
// const logger = new InMemoryLogger()
// const service = new ReportService(logger)
// service.generateReport('Weekly Summary')
diff --git a/extra/02.inventory-manager/README.mdx b/extra/02.inventory-manager/README.mdx
index 8026779..2fed225 100644
--- a/extra/02.inventory-manager/README.mdx
+++ b/extra/02.inventory-manager/README.mdx
@@ -30,15 +30,63 @@ npm run dev
### In `src/app.tsx`
-1. Verify the UI updates once class methods are implemented
-2. Check the TODO callouts to confirm you've completed each piece
+1. Replace the placeholder item objects with real class instances
+2. Wire `InventoryManager` + loggers so the Inventory Actions panel shows
+ live data
+
+## Concrete examples (suggested fixtures)
+
+Use these field values (constructor shape is up to you) so the UI has something
+concrete to show:
+
+### Electronics β Laptop
+
+Fields: `name: 'Laptop'`, `basePrice: 1000`, `brand: 'Nova'`, `model: 'X15'`,
+`serialNumber: 'SN-100'`, `location: 'Aisle 3'`, `discountPercent: 10`
+
+Observable results:
+
+- `getDescription()` includes the name, brand, and model
+- `calculatePrice(2)` β `1800` with those price/discount values
+- `getTrackingInfo()` includes serial and location details
+- `updateLocation('Warehouse')` is reflected in later tracking info
+
+### Clothing β Hoodie
+
+Fields: `name: 'Hoodie'`, `basePrice: 40`, `size: 'M'`, `color: 'Navy'`,
+`discountPercent: 0`
+
+- `getDescription()` includes the name, size, and color
+- `calculatePrice(1)` β `40` when discount is `0`
+
+### Perishable β Apples
+
+Fields: `name: 'Apples'`, `basePrice: 3`, `expirationDate: '2026-08-01'`
+
+- `getDescription()` includes the name and expiration date
+
+### Logging + InventoryManager
+
+`Logger.log(message: string): string` returns the same `message` it received
+(no required prefix). `ConsoleLogger` also prints that message.
+`InMemoryLogger` stores that same message for `getLogs()`.
+
+```ts
+const manager = new InventoryManager(new ConsoleLogger())
+manager.receiveStock('Laptop', 5)
+// log output mentions receiving Laptop / 5
+
+const memoryLogger = new InMemoryLogger()
+new InventoryManager(memoryLogger).shipStock('Hoodie', 2)
+memoryLogger.getLogs().length // >= 1
+```
## Tips
π° Start with the base `InventoryItem` class. Many other classes depend on it.
-π° When you implement pricing, be consistent about using `basePrice`,
-`quantity`, and `discountPercent`.
+π° For sellable pricing, combine `basePrice`, order `quantity`, and
+`discountPercent` so the Laptop example above yields `1800` for quantity `2`.
π° Use the logger classes to see the effects of dependency injection.
diff --git a/extra/02.inventory-manager/src/classes.ts b/extra/02.inventory-manager/src/classes.ts
index 91e2a8c..2d9d5ee 100644
--- a/extra/02.inventory-manager/src/classes.ts
+++ b/extra/02.inventory-manager/src/classes.ts
@@ -18,9 +18,10 @@
// ============================================================================
// π¨ Implement InventoryItem with:
-// - private fields #id and #quantity
+// - private fields for id and quantity (use #)
// - public fields name and basePrice
// - methods: getId, getQuantity, adjustQuantity, getDescription
+// Example: getDescription() can start as just the item name
// ============================================================================
// Inventory Item Types
@@ -28,17 +29,23 @@
// π¨ Implement Electronics to extend InventoryItem and implement Sellable, Trackable
// - fields: brand, model, serialNumber, warrantyMonths, location, discountPercent
-// - methods: calculatePrice, applyDiscount, getTrackingInfo, updateLocation
-// - override getDescription to include brand/model
+// - calculatePrice / applyDiscount
+// Example check: basePrice 1000, discountPercent 10, calculatePrice(2) β 1800
+// - getTrackingInfo() includes serialNumber and location
+// - updateLocation(location) is reflected in later tracking reads
+// - override getDescription to include name, brand, and model
+// (exact wording is up to you β e.g. it might mention Laptop / Nova / X15)
// π¨ Implement Clothing to extend InventoryItem and implement Sellable
// - fields: size, color, discountPercent
-// - methods: calculatePrice, applyDiscount
-// - override getDescription to include size/color
+// - same pricing behavior as Electronics
+// - override getDescription to include name, size, and color
+// (exact wording is up to you)
// π¨ Implement Perishable to extend InventoryItem
// - field: expirationDate
-// - override getDescription to include expiration date
+// - override getDescription to include name and expiration date
+// (exact wording is up to you)
// ============================================================================
// Composition & Dependency Injection
@@ -46,17 +53,19 @@
// π¨ Create a Logger class with:
// - log(message: string): string
+// - Returns the same message it received (no required prefix)
// π¨ Extend Logger with ConsoleLogger
-// - override log to call console.log
+// - override log to print the message and return that same message
// π¨ Extend Logger with InMemoryLogger
-// - private field #logs
-// - override log to store logs
-// - getLogs(): Array
+// - store each message privately (the same string passed to log)
+// - return that same message from log()
+// - getLogs(): Array returns the stored messages
// π¨ Implement InventoryManager using dependency injection for logging
-// - constructor takes Logger
-// - receiveStock and shipStock log actions
+// - constructor(logger: Logger)
+// - receiveStock(name, quantity) logs a receive action that mentions name/qty
+// - shipStock(name, quantity) logs a ship action that mentions name/qty
export {}