Chapter 42: TypeScript Exercises
TypeScript Exercises guide — written exactly as if we are sitting together in a small coding workshop right now (February 2026).
I prepared progressive exercises that start very easy and become gradually more realistic / production-like. Each block contains:
- short goal explanation
- the exercise task
- difficulty rating
- hints (only read if stuck)
- example solution (scroll down only after you tried!)
You can solve everything directly in the official TypeScript Playground: 👉 https://www.typescriptlang.org/play
Level 1 – Warm-up (very beginner friendly)
Exercise 1.1 – Fix inference & add basic types Difficulty: ★☆☆☆☆
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Goal: make the code type-safe without using 'any' anywhere let count; count = 42; count = "forty-two"; // should become error const user = { id: 1, name: "Sara", isActive: true, scores: [92, 85, 78] }; function greet(name, age) { return `Hello ${name}, you are ${age} years old`; } console.log(greet("Rahul", 28)); console.log(greet(28, "Rahul")); // should become error |
Exercise 1.2 – Literal union type + function Difficulty: ★★☆☆☆
Create type Size that only allows these exact values:
“xs” | “sm” | “md” | “lg” | “xl” | “2xl”
Then write function getPadding(size: Size) that returns:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
xs → "0.25rem" sm → "0.5rem" md → "1rem" lg → "1.5rem" xl → "2rem" 2xl → "3rem" |
Make sure VS Code autocompletes the possible values when you type getPadding(“…
Level 2 – Functions, objects, unions, generics
Exercise 2.1 – Generic first/last element Difficulty: ★★☆☆☆
Write two generic functions:
|
0 1 2 3 4 5 6 7 |
function first<T>(arr: T[]): T | undefined function last<T>(arr: T[]): T | undefined |
So that these lines have correct types:
|
0 1 2 3 4 5 6 7 8 |
first([1,2,3]) // number | undefined first(["a","b","c"]) // string | undefined last([{id:1}, {id:2}]) // {id: number} | undefined |
Exercise 2.2 – Deep partial utility type Difficulty: ★★★☆☆
Create type DeepPartial<T> so this is valid:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
interface Product { id: number; name: string; price: number; stock: { available: number; warehouse: string; locations: string[]; }; tags: string[]; } const partialUpdate: DeepPartial<Product> = { name: "Wireless Mouse", stock: { available: 45 // warehouse and locations can be missing → ok }, tags: ["electronics"] // partial array is ok }; |
Level 3 – Discriminated unions, narrowing, exhaustiveness
Exercise 3.1 – State machine with exhaustive check Difficulty: ★★★☆☆
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type FetchState = | { status: "idle" } | { status: "loading" } | { status: "success"; data: string[] } | { status: "error"; message: string }; function getDisplayMessage(state: FetchState): string { // Use switch + default case that forces you to handle every variant // → you should get a compile error if you forget one case } |
Exercise 3.2 – Branded types for safety Difficulty: ★★★★☆
Make sure these two calls are not accidentally swapped:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
type UserId = string & { __brand: "UserId" }; type OrderId = string & { __brand: "OrderId" }; function lookupUser(id: UserId) { /* ... */ } function lookupOrder(id: OrderId) { /* ... */ } const uid = "usr_abc123" as UserId; const oid = "ord_456def" as OrderId; lookupUser(uid); // ok lookupUser(oid); // ← should be compile error! |
Level 4 – Real-world-ish exercises
Exercise 4.1 – useLocalStorage hook with strong types
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
function useLocalStorage<T>(key: string, initialValue: T) { // return tuple [value: T, setValue: (newValue: T | ((prev: T) => T)) => void] // bonus: preserve literal types when possible } // Expected usage should feel natural: const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light"); |
Exercise 4.2 – Generic table component props
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
interface Column<T> { key: keyof T & string; header: string; render?: (value: T[keyof T], row: T) => React.ReactNode; } interface TableProps<T> { data: T[]; columns: Column<T>[]; } function GenericTable<T extends object>(props: TableProps<T>) { // ... } // Should give nice autocompletion + errors const users = [{ id: 1, name: "Sara", age: 28 }]; <GenericTable data={users} columns={[ { key: "name", header: "Name" }, { key: "age", header: "Age", render: (age) => `${age} years` }, // { key: "email" } → should error – no email field ]} /> |
Bonus – Pick one harder challenge
Challenge A – Deep key paths union
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
type User = { id: number; profile: { name: string; address: { city: string; zip: number }; }; roles: string[]; }; type Paths<T> = /* your type here */ // Expected result: type UserPaths = Paths<User>; // → "id" | "profile" | "profile.name" | "profile.address" | "profile.address.city" | "profile.address.zip" | "roles" |
Challenge B – Type-safe route builder
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const routes = { userProfile: "/users/:id", postDetail: "/posts/:postId/comments/:commentId" } as const; function buildRoute< Route extends keyof typeof routes, Params extends Record<string, string> >( route: Route, params: Params ): string { // replace :param with real values // throw / error at compile time if wrong keys or missing params } |
How to get the most out of these exercises
- Do 2–4 exercises per day — not more (quality > quantity)
- Always try first without looking at hints/solution
- After finishing → compare your version with the example solution Ask yourself: „Which version is easier to read / refactor / extend?“
- Put your solutions into a GitHub repo called ts-exercises
- After ~30–40 exercises → start applying these patterns in your real project
Which level / exercise would you like to start with right now? Or tell me your current skill level (beginner / intermediate / advanced) and I can pick 5 exercises that match best.
Ready when you are 😄
