- Running code in TypeScript with Node.js
- Basic TypeScript types (number, string, boolean, etc.)
- Functions and type annotations
- Type safety and compile-time checks
- Factory functions
- Smart constructors
- Value objects
- Entities
- Observer Pattern
| ---- Concept -------------- | -------- What it solves --------------------------------- |
|---|---|
| -- Branded Types -- | Prevent mixing up values that share the same primitive type (e.g., Email vs Phone) |
| -- Smart Constructors -- | Validate business rules at creation time so invalid values never exist |
| -- Value Objects -- | Model domain concepts (Money, Email, OperatingHours) as immutable types defined by their value |
| -- Entities -- | Model things with identity and lifecycle (Table, Order) that enforce their own invariants |
| -- Parse, Don't Validate -- | Transform raw input into guaranteed-valid types at the boundary, then trust the types everywhere else |
ex-01/
index.ts # CLI runner -- runs functions here
# Install dependencies
npm install
# Run the exercises
npm run exercisesSelect an exercise (1-8) or run all of them (9). After running, open silent_errors.log to see every silent bug that was triggered.
Each exercise file follows the same structure:
- Header comments explain the anti-pattern and the DDD concept that fixes it.
- A HINT block shows a concrete code example of the solution pattern.
- A TODO comment tells you exactly what to change.
- The buggy code demonstrates the problem in action.
- Read the header comments to understand the anti-pattern and the fix.
- Implement the branded type / Value Object / Entity described in the hint.
- Refactor the exercise function to use your new types.
- Verify that the previously-silent bugs now produce either compile-time errors or runtime exceptions.
Before (primitive):
type MenuItem = {
name: string
price: number // accepts -50, no complaints
quantity: number
}After (branded type):
type Price = number & { readonly __brand: unique symbol }
function createPrice(amount: number): Price {
if (amount < 0) throw new Error("Price cannot be negative")
if (amount > 10_000) throw new Error("Price exceeds maximum")
return amount as Price
}
type MenuItem = {
name: string
price: Price // only accepts values from createPrice()
quantity: number
}Now price: -50 is a compile-time error (a raw number is not assignable to Price), and createPrice(-50) throws at runtime. The bug is impossible.
- Make illegal states unrepresentable. If a value should never be negative, make the type reject negatives. If two fields should not be swappable, give them different types.
- Push validation to the boundary. Parse raw input (user forms, API responses, database rows) into strong domain types at the edge of your system. Inside the domain, trust the types.
- Domain logic belongs inside domain objects. An
OperatingHoursobject should know how to answer "am I open at 2 AM?". AMoneyobject should know how to add two amounts in the same currency. Don't scatter this logic across utility functions. - Types are documentation. When a function takes
Priceinstead ofnumber, its intent is clear without comments. When a function takesEmailinstead ofstring, you know the value has been validated.