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:
- Step 1 — model the data: a
Categoryunion and aProductinterface (types.ts). - Step 2 — build a typed
Cartclass that keeps its items private (cart.ts). - Step 3 — add a reusable generic helper that works for any list (helpers.ts).
- Step 4 — write a discount function that takes a union and narrows it with
typeof(discount.ts). - 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.
// 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.
// 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.
// 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.
// 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.
// 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(stricton). - Add a new
Category(e.g.'clothing') and a product that uses it. - Add a
CartmethoditemsIn(category: Category): Product[]that returns only products in one category. - Add a
'tenth'option toapplyDiscount(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?
✍️ Practice
- Add a
mostExpensive(): Product | undefinedmethod to the Cart and print its result. - Add a second discount style (e.g. a percentage like
'quarter') and narrow the union to support it.
🏠 Homework
- 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.