Hands-on labs¶
These labs pull together concepts from the learning path into realistic, self-contained projects. Work through them in order, or jump to the one that matches your current skill.
Lab 1: e-commerce product catalog¶
Build a typed product catalog with filtering and sorting.
Requirements¶
interface Product { id: string; name: string; price: number; category: 'electronics' | 'clothing' | 'home'; inStock: boolean }- A function
createProduct(data: Omit<Product, 'id'>): Productthat assigns a randomid. - A
filterByCategory(products, category)that returns only matching products. - A
sortByPrice(products, direction: 'asc' | 'desc')function. - A
totalInStockValue(products)that sums the price of in-stock products.
Stretch goals¶
- Add
readonlyto theidfield. - Use a generic
paginate<T>(items: T[], page: number, pageSize: number): T[]. - Add a
Result<Product[]>return type for a search function.
Try it in the playground¶
interface Product {
id: string
name: string
price: number
category: 'electronics' | 'clothing' | 'home'
inStock: boolean
const products: Product[] = [
{ id: '1', name: 'Phone', price: 999, category: 'electronics', inStock: true },
]
console.log(products[0].name)
Lab 2: task manager with narrowing¶
Build a small task manager using discriminated unions.
Requirements¶
type Task = | { kind: 'todo'; id: string; title: string } | { kind: 'in-progress'; id: string; title: string; startedAt: Date } | { kind: 'done'; id: string; title: string; completedAt: Date }function describe(task: Task): stringusing aswitchonkind.function advance(task: Task): Taskthat transitions a task to the next state.- An array of mixed tasks and a
console.tablesummary.
Stretch goals¶
- Use
constassertions for state literals. - Add
totalTime(task: Task): number | nullthat only returns a value for completed tasks.
Try it in the playground¶
type Task =
| { kind: 'todo'; id: string; title: string }
| { kind: 'done'; id: string; title: string; completedAt: Date }
function describe(task: Task): string {
switch (task.kind) {
case 'todo':
return task.title
case 'done'
return task.title
}
}
console.log(describe({ kind: 'todo', id: '1', title: 'Read' }))
Lab 3: typed HTTP client¶
Build a wrapper around fetch that returns typed results.
Requirements¶
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string }async function getJson<T>(url: string): Promise<ApiResult<T>>usingfetchandresponse.json().interface Post { userId: number; id: number; title: string; body: string }- Fetch posts from
https://jsonplaceholder.typicode.com/postsand handle errors.
Stretch goals¶
- Add a small runtime validation check for the shape of
Post. - Add a
postJson<T, U>(url: string, body: U): Promise<ApiResult<T>>.
Try it in the playground¶
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string }
function message(result: ApiResult<string>): string {
if (result.ok) {
return result.data
}
return result.error
console.log(message({ ok: true, data: 'Loaded' }))
Lab 4: React expense tracker¶
Build a small React app that tracks expenses.
Requirements¶
interface Expense { id: string; amount: number; category: string; date: string }- Components:
ExpenseForm,ExpenseList,ExpenseSummary. - State for the list of expenses and derived totals.
- Type-safe event handlers for form inputs.
- Filter expenses by category.
Stretch goals¶
- Use
useReducerwith a typed action union. - Add
localStoragepersistence with a validation step before loading.
Try it in the playground¶
interface Expense {
id: string
amount: number
category: string
}
const total = (expenses: Expense[]): number => {
return expenses.reduce((sum, e) => sum + e.amount)
}
console.log(total([
{ id: '1', amount: 10, category: 'food' },
{ id: '2', amount: 20, category: 'food' },
]))
Lab 5: library class model¶
Use classes to model a small library system.
Requirements¶
abstract class Item { id: string; title: string; abstract description(): string }class Bookandclass MagazineextendingItem.interface Borrowable { borrow(): void; returnItem(): void }.- A
Libraryclass that ownsItem[]and can search by title.
Stretch goals¶
- Add
privateandprotectedfields. - Implement a
borrowhistory withreadonlyarrays.
Try it in the playground¶
abstract class Item {
constructor(public id: string, public title: string) {}
abstract description(): string
}
class Book extends Item {
constructor(id: string, title: string, public author: string) {
super(id, title)
}
description(): string {
return `${this.title} by ${this.author}`
}
}
const book = new Book('1', 'TypeScript', 'Ada'
console.log(book.description())
How to verify your work¶
For each lab:
- Write the types first.
- Implement the functions or components.
- Run
npx tsc --noEmitto catch type errors without running anything. - Add sample data and run the program or the dev server.
- Deliberately introduce a type error and read the message. This is the fastest way to learn what TypeScript is protecting you from.