← Builder lesson
Lab

Builder

type Stop = { place: string; nights: number };
type TripPlan = { name: string; kind: string; stops: Stop[]; nights: number };

// The builder owns the draft: setters replace, addStop appends.
class TripBuilder {
  private tripName = '';
  private tripKind = 'day-trip';
  private stops: Stop[] = [];

  name(value: string): this {
    this.tripName = value;
    return this;
  }
  kind(value: string): this {
    this.tripKind = value;
    return this;
  }
  addStop(stop: Stop): this {
    this.stops.push({ ...stop });
    return this;
  }

  // build checks the whole draft, then returns the plan.
  build(): TripPlan {
    const nights = this.stops.reduce((total, stop) => total + stop.nights, 0);
    if (this.tripKind === 'day-trip' && nights > 0)
      throw new Error('a day trip cannot include an overnight stay');
    if (this.tripKind === 'overnight' && nights === 0)
      throw new Error('an overnight trip needs at least one night');
    const stops = this.stops.map((stop) => ({ ...stop }));
    return { name: this.tripName, kind: this.tripKind, stops, nights };
  }
}

const show = (plan: TripPlan) => {
  const route = plan.stops.map((stop) => stop.place).join(' → ') || 'no stops';
  return `${plan.name}, ${plan.kind}: ${route}, nights=${plan.nights}`;
};

const builder = new TripBuilder().name('Weekend route');
builder.addStop({ place: 'Harbor', nights: 0 });
const first = builder.build();

builder.addStop({ place: 'Old town', nights: 1 });
let blocked = '';
try {
  builder.build();
} catch (error) {
  blocked = (error as Error).message;
}

builder.kind('overnight');
const second = builder.build();

console.log(`first: ${show(first)}`);
console.log(`blocked: ${blocked}`);
console.log(`second: ${show(second)}`);
Builder’s draft mutable

Waiting for the draft

kind—
Stops appear when recorded
build · validate + hand off
firstTripPlan

Not observed yet

secondTripPlan

Not observed yet

Step to observe the first assembly call.