
TypeScript Exercises: 20 Practice Problems with Solutions
TypeScript is learned by fighting the compiler and winning. These problems focus on the type system: annotations, interfaces, unions, generics, narrowing and the utility types you will use every day.
Run your answer in the editor below. Type errors show up in the output, which is exactly the feedback you want while practising types.
Run your answer here
Type your solution, press Run, and use the Input box when a problem asks you to read from standard input.
How to practise so it actually sticks
Attempt the problem before opening the solution, even if your first version is clumsy. A working ugly answer teaches more than a beautiful one you read. When you get stuck for more than five minutes, read the hint — not the solution — and try again.
After you pass, open the solution and ask what is different about it. Shorter? Fewer variables? A built-in you did not know? That comparison is where most of the growth happens. Then change the problem slightly: sort the other direction, handle an empty input, read a value instead of hard-coding it.
Aim for three to five problems a day rather than thirty in one sitting. Spacing practice over days is what moves syntax from "I can look it up" to "my fingers know it".
The exercises
Showing 20 of 20 exercises.
- BeginnerTypes & basics
1. Annotate basic values
Declare a string, a number and a boolean with explicit type annotations and log all three.
Hint: const name: string = "Ada";
Show solution
const name: string = "Ada"; const age: number = 36; const active: boolean = true; console.log(name, age, active); - BeginnerTypes & basics
2. Type a function
Write add(a, b) with number parameters and a number return type.
Hint: function add(a: number, b: number): number.
Show solution
function add(a: number, b: number): number { return a + b; } console.log(add(2, 3)); - BeginnerTypes & basics
3. Typed arrays
Create a string[] of three languages and log them uppercased.
Hint: map((s) => s.toUpperCase()).
Show solution
const langs: string[] = ["ts", "go", "rust"]; console.log(langs.map((s) => s.toUpperCase())); - BeginnerInterfaces & types
4. Describe an object with an interface
Define interface User { id: number; name: string; email?: string } and create one user.
Hint: The ? marks an optional property.
Show solution
interface User { id: number; name: string; email?: string; } const u: User = { id: 1, name: "Ada" }; console.log(u); - BeginnerInterfaces & types
5. Union types
Write format(v: string | number) that returns "n=5" for numbers and the string itself otherwise.
Hint: Narrow with typeof v === "number".
Show solution
function format(v: string | number): string { return typeof v === "number" ? `n=${v}` : v; } console.log(format(5), format("hi")); - IntermediateInterfaces & types
6. Literal union as an enum
Define type Status = "todo" | "doing" | "done" and a function returning a label for each.
Hint: A switch over the union gets exhaustiveness checking.
Show solution
type Status = "todo" | "doing" | "done"; function label(s: Status): string { switch (s) { case "todo": return "Not started"; case "doing": return "In progress"; case "done": return "Finished"; } } console.log(label("doing")); - IntermediateGenerics
7. A generic function
Write first<T>(items: T[]): T | undefined and use it on numbers and strings.
Hint: The type parameter goes before the parentheses.
Show solution
function first<T>(items: T[]): T | undefined { return items[0]; } console.log(first([1, 2, 3]), first(["a", "b"])); - IntermediateGenerics
8. Constrain a generic
Write pluck<T, K extends keyof T>(rows: T[], key: K) returning an array of that field.
Hint: keyof T gives the valid key names.
Show solution
function pluck<T, K extends keyof T>(rows: T[], key: K): T[K][] { return rows.map((r) => r[key]); } console.log(pluck([{ n: "a" }, { n: "b" }], "n")); - IntermediateGenerics
9. Partial, Pick and Omit
From a Product type, build a PatchProduct with Partial and a ProductCard with Pick.
Hint: Partial<T> makes every property optional.
Show solution
type Product = { id: number; title: string; price: number; stock: number }; type PatchProduct = Partial<Product>; type ProductCard = Pick<Product, "title" | "price">; const patch: PatchProduct = { price: 9 }; const card: ProductCard = { title: "Mug", price: 9 }; console.log(patch, card); - IntermediateGenerics
10. Record for lookup tables
Build a Record<string, number> of word lengths for three words.
Hint: Record<K, V> describes an object with keys K and values V.
Show solution
const words = ["hi", "tree", "hello"]; const lengths: Record<string, number> = {}; for (const w of words) lengths[w] = w.length; console.log(lengths); - AdvancedNarrowing
11. Write a type guard
Write isUser(v: unknown): v is User and use it to safely read a name from unknown data.
Hint: Check typeof v === "object" && v !== null && "name" in v.
Show solution
type User = { name: string }; function isUser(v: unknown): v is User { return typeof v === "object" && v !== null && "name" in v && typeof (v as User).name === "string"; } const data: unknown = { name: "Ada" }; console.log(isUser(data) ? data.name : "unknown"); - AdvancedNarrowing
12. Discriminated union
Model a Result as { ok: true; value: number } | { ok: false; error: string } and handle both.
Hint: The shared literal field tells TypeScript which branch you are in.
Show solution
type Result = { ok: true; value: number } | { ok: false; error: string }; function show(r: Result): string { return r.ok ? `value ${r.value}` : `failed: ${r.error}`; } console.log(show({ ok: true, value: 7 }), show({ ok: false, error: "nope" })); - IntermediateNarrowing
13. readonly and as const
Create a frozen tuple of roles with as const and derive a union type from it.
Hint: typeof roles[number] gives the element union.
Show solution
const roles = ["admin", "editor", "viewer"] as const; type Role = typeof roles[number]; const r: Role = "editor"; console.log(roles, r); - IntermediateClasses
14. A typed class
Write class Stack<T> with push, pop and size, then use it with numbers.
Hint: Keep a private items: T[].
Show solution
class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } get size(): number { return this.items.length; } } const s = new Stack<number>(); s.push(1); s.push(2); console.log(s.pop(), s.size); - IntermediateClasses
15. Implement an interface
Define interface Shape { area(): number } and two classes implementing it.
Hint: Use the implements keyword.
Show solution
interface Shape { area(): number } class Square implements Shape { constructor(private side: number) {} area() { return this.side ** 2; } } class Circle implements Shape { constructor(private r: number) {} area() { return Math.PI * this.r ** 2; } } console.log(new Square(3).area(), new Circle(1).area().toFixed(2)); - AdvancedAsync
16. Typed async function
Write loadUser(id: number): Promise<User> that resolves a fake user after a delay.
Hint: An async function returns Promise<T>.
Show solution
type User = { id: number; name: string }; const delay = (ms: number) => new Promise<void>((res) => setTimeout(res, ms)); async function loadUser(id: number): Promise<User> { await delay(50); return { id, name: "Ada" }; } loadUser(1).then((u) => console.log(u)); - AdvancedAsync
17. Catch clause is unknown
Catch an error and log its message safely, given catch variables are typed unknown.
Hint: Narrow with err instanceof Error.
Show solution
function risky(): never { throw new Error("boom"); } try { risky(); } catch (err: unknown) { console.log(err instanceof Error ? err.message : String(err)); } - AdvancedAdvanced types
18. Write a mapped type
Create Nullable<T> that makes every property T[K] | null.
Hint: { [K in keyof T]: T[K] | null }.
Show solution
type Nullable<T> = { [K in keyof T]: T[K] | null }; type Row = { id: number; title: string }; const r: Nullable<Row> = { id: 1, title: null }; console.log(r); - AdvancedAdvanced types
19. Generic group-by
Write groupBy<T>(rows: T[], key: (row: T) => string): Record<string, T[]>.
Hint: Build the object with reduce, creating arrays lazily.
Show solution
function groupBy<T>(rows: T[], key: (row: T) => string): Record<string, T[]> { return rows.reduce<Record<string, T[]>>((acc, row) => { const k = key(row); (acc[k] ||= []).push(row); return acc; }, {}); } console.log(groupBy(["hi", "cat", "tree"], (w) => String(w.length))); - AdvancedAdvanced types
20. Exhaustiveness with never
Add a default branch that assigns the value to never so new union members become compile errors.
Hint: const _exhaustive: never = value;
Show solution
type Shape = { kind: "square"; side: number } | { kind: "circle"; r: number }; function area(s: Shape): number { switch (s.kind) { case "square": return s.side ** 2; case "circle": return Math.PI * s.r ** 2; default: { const _exhaustive: never = s; return _exhaustive; } } } console.log(area({ kind: "square", side: 4 }));
What to do next
If a whole topic feels shaky, go back to that chapter in the 17-chapter TypeScript course and re-read it, then return here. When the advanced problems feel routine, take the final TypeScript quiz and claim your certificate, or open the TypeScript online compiler and build something of your own.