Practical Projects
You have learned the pieces. These projects make them stick. Each one focuses on a few TypeScript skills — start with the first and work down.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Model a domain with interfaces, unions and literal types
- Write reusable generic code
- Handle external data safely
- Migrate JavaScript to TypeScript
- Build portfolio pieces that show TypeScript skills
Explanation#
Project 1 — Expense tracker (beginner): Expense interface, Category literal union, functions to add, filter and total expenses, Record<Category, number> for summaries.
Project 2 — Generic data table (intermediate): a function or React component that sorts and filters any T[] with columns typed as { key: keyof T; label: string }[].
Project 3 — Typed API client (intermediate): api<T>() helper, ApiError class, request/response DTOs derived with Omit and Partial, type guards for responses.
Project 4 — Form validator (intermediate): Validator types, a schema typed as Record<keyof FormValues, Validator[]>, results typed as Partial<Record<keyof FormValues, string>>.
Project 5 — Migration (advanced): take an old JavaScript project, add tsconfig with allowJs, rename files one at a time, then enable strict and fix the errors.
Code example: Project 1 — Typed expense tracker (solution)#
const CATEGORIES = ['food', 'travel', 'bills', 'shopping'] as const;
type Category = (typeof CATEGORIES)[number];
interface Expense {
readonly id: number;
title: string;
amount: number;
category: Category;
date: Date;
}
type NewExpense = Omit<Expense, 'id'>;
class ExpenseTracker {
#expenses: Expense[] = [];
#nextId = 1;
add(data: NewExpense): Expense {
if (data.amount <= 0) throw new Error('Amount must be positive');
const expense = { id: this.#nextId++, ...data };
this.#expenses.push(expense);
return expense;
}
byCategory(category: Category): Expense[] {
return this.#expenses.filter(e => e.category === category);
}
summary(): Record<Category, number> {
const totals = Object.fromEntries(CATEGORIES.map(c => [c, 0])) as Record<Category, number>;
for (const e of this.#expenses) totals[e.category] += e.amount;
return totals;
}
}
const tracker = new ExpenseTracker();
tracker.add({ title: 'Lunch', amount: 250, category: 'food', date: new Date() });
tracker.add({ title: 'Metro card', amount: 500, category: 'travel', date: new Date() });
tracker.add({ title: 'Dinner', amount: 400, category: 'food', date: new Date() });
console.log(tracker.summary());
tracker.add({ title: 'Movie', amount: 300, category: 'fun', date: new Date() });
// ✗ Type '"fun"' is not assignable to type 'Category'
{ food: 650, travel: 500, bills: 0, shopping: 0 } — and a compile error for the unknown "fun" category.This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- CATEGORIES as const is the single source for both the runtime list and the Category type.
- Expense uses readonly id and the Category union.
- NewExpense is derived with Omit, so callers never invent ids.
- Private #fields keep the data safe inside the class.
- summary() returns a Record that is guaranteed to contain every category.
- Invalid categories are rejected at compile time.
Real-world use case#
These projects mirror real tasks: modelling domain data, building reusable UI pieces, talking to APIs and modernising old code — exactly what TypeScript jobs involve.
Common mistakes#
Use unknown and narrow it, or ask “what shape is this really?” and write the type. Each any you avoid is a skill learned.
Clever conditional types are fun but hard to read. Prefer simple interfaces and unions until you need more.
Best practices#
- Start each project by writing the types first
- Keep strict mode on
- Derive types instead of duplicating them
- Put projects on GitHub with a README explaining the type design
Remember this#
Types first, code second — the types are your design.
One source of truth (as const, interfaces) → derive the rest.
Stuck? unknown + narrowing, not any.
Practice exercise#
Your task: Project 2 starter: write sortBy<T>(items: T[], key: keyof T, dir: "asc" | "desc" = "asc"): T[] that returns a sorted copy.
Need a hint?
Show solution
function sortBy<T>(items: T[], key: keyof T, dir: 'asc' | 'desc' = 'asc'): T[] {
const factor = dir === 'asc' ? 1 : -1;
return [...items].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0) * factor);
}
const people = [{ name: 'Ravi', age: 30 }, { name: 'Asha', age: 24 }];
sortBy(people, 'age'); // Asha, Ravi
sortBy(people, 'email'); // ✗ not a key of the items
Mini quiz#
Why define CATEGORIES as const and derive Category from it?
Want more? Practise all TypeScript MCQs · TypeScript interview questions
My notes#
Saved in this browser.