ProjectCore· 120 min read

Project: A Fully Typed Shopping Cart

Bring it all together: build a small, fully typed shopping cart with interfaces, a class, generics and narrowing.

What you will learn

  • Model real data with interfaces and types
  • Build a typed class with private state and methods
  • Combine generics, unions and narrowing in one program

What you will build

A small shopping cart program in TypeScript. It will hold typed products, let you add and remove them, total the price, and apply a discount. Every part uses the features from this course — and the type checker will guard you the whole way.

Before we write any code, here is the whole build laid out in order so you always know where you are:

  1. Step 1 — model the data: a Category union and a Product interface (types.ts).
  2. Step 2 — build a typed Cart class that keeps its items private (cart.ts).
  3. Step 3 — add a reusable generic helper that works for any list (helpers.ts).
  4. Step 4 — write a discount function that takes a union and narrows it with typeof (discount.ts).
  5. Step 5 — wire it all together in a main file, then compile and run it (app.ts).

Step 1 — model the data with a type and an interface

First, describe what a product looks like, and the small set of valid categories.

Step 1: a Category union and a Product interface
// types.ts
export type Category = 'electronics' | 'books' | 'food';

export interface Product {
  readonly id: number;
  name: string;
  price: number;
  category: Category;
}

Note: Output: (No output — these are type definitions.) Category limits the allowed values to three; Product fixes the shape, with a readonly id so it cannot change once set.

Step 2 — a typed Cart class with private state

The cart keeps its items private so nothing outside can mess with the list directly. Public methods are the only way in.

Step 2: a Cart class guarding a private items list
// cart.ts
import { Product } from './types';

export class Cart {
  private items: Product[] = [];

  add(product: Product): void {
    this.items.push(product);
  }

  remove(id: number): void {
    this.items = this.items.filter(p => p.id !== id);
  }

  total(): number {
    return this.items.reduce((sum, p) => sum + p.price, 0);
  }

  count(): number {
    return this.items.length;
  }
}

Note: Output: (No output yet.) items is private, so callers must use add, remove, total and count. Each method is typed, so misuse is caught at compile time.

Step 3 — a generic helper

Add a reusable, generic helper that works for any array — products, numbers, anything. This is the generics idea earning its keep.

Step 3: a generic lastOf that works for any list
// helpers.ts
export function lastOf<T>(items: T[]): T | undefined {
  return items[items.length - 1];
}

Note: Output: (No output yet.) lastOf<T> returns the last item of any array while keeping its real type. It returns T | undefined because the list might be empty.

Step 4 — a discount using a union and narrowing

A discount can be a flat amount (a number) or a label like 'half' (a string). We use a union for the input and narrow it with typeof to handle each case.

Step 4: a union input narrowed with typeof
// discount.ts
export function applyDiscount(total: number, discount: number | 'half'): number {
  if (typeof discount === 'number') {
    return total - discount;       // narrowed: a flat amount
  }
  return total / 2;                // narrowed: the 'half' label
}

Note: Output: (No output yet.) The parameter is number | 'half'. The typeof guard tells TypeScript which one we have, so each branch is safe.

Step 5 — put it together and run it

Now the main file imports everything and uses the cart end to end.

Step 5: the finished, fully typed program
// app.ts
import { Cart } from './cart';
import { lastOf } from './helpers';
import { applyDiscount } from './discount';

const cart = new Cart();

cart.add({ id: 1, name: 'Keyboard', price: 1200, category: 'electronics' });
cart.add({ id: 2, name: 'TS Handbook', price: 800, category: 'books' });
cart.add({ id: 3, name: 'Coffee', price: 300, category: 'food' });

cart.remove(3);   // changed our mind about coffee

const subtotal = cart.total();
console.log('Items:', cart.count());
console.log('Subtotal:', subtotal);
console.log('After flat 200 off:', applyDiscount(subtotal, 200));
console.log('After half off:', applyDiscount(subtotal, 'half'));
console.log('Last item id:', lastOf([1, 2]));

Note: Output: Items: 2 Subtotal: 2000 After flat 200 off: 1800 After half off: 1000 Last item id: 2 Three products were added, one removed, leaving 2 items totalling 2000. The flat discount took off 200; the 'half' discount halved it; and the generic lastOf returned 2. Every line was type-checked.

Your tasks

  • Build all five files and compile them with a tsconfig.json (strict on).
  • Add a new Category (e.g. 'clothing') and a product that uses it.
  • Add a Cart method itemsIn(category: Category): Product[] that returns only products in one category.
  • Add a 'tenth' option to applyDiscount (10% off) and narrow for it.
  • Try one mistake on purpose (a wrong price type, an invalid category) and read the error TypeScript gives you.

Tip: Build in small steps and compile after each file. A small cart that fully works beats a big one that will not compile. Run npx tsc --watch so errors appear the moment you save.

Watch out: Keep the cart's items field private. If you expose it, other code could add an item with a wrong shape and bypass all your typing — defeating the point.

Q. In the discount function, why is the parameter typed number | 'half' with a typeof check inside?

Answer: The union lets the function accept two kinds of discount, and the typeof guard narrows to the right one in each branch — combining unions and narrowing safely.

✍️ Practice

  1. Add a mostExpensive(): Product | undefined method to the Cart and print its result.
  2. Add a second discount style (e.g. a percentage like 'quarter') and narrow the union to support it.

🏠 Homework

  1. Extend the project into a typed to-do list instead: a Task interface (readonly id, title, done, priority union), a TaskList class with private tasks, and methods to add, complete and count tasks. Write a short note on which TypeScript features you used and why.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →