Practice TypeScript
You’ve read the lessons — now prove it. Take the quiz for instant feedback, then work the coding exercises. Try each one yourself before you reveal the solution.
Quick quiz
17 questions- 1
Which TypeScript type represents a value that can be either a string or a number?
- 2
What does TypeScript infer as the type of
xinconst x = [1, 2, 3];? - 3
What is the key difference between a
typealias and aninterfacein TypeScript? - 4
Given
enum Direction { Up, Down, Left, Right }, what is the value ofDirection.Down? - 5
What does the
?in a function parameterfunction greet(name?: string)mean? - 6
What does the
readonlymodifier do on an interface property? - 7
What is the output type of this function?
function identity<T>(value: T): T { return value; }When called asidentity(42), what does TypeScript infer forT? - 8
Which utility type creates a new type where all properties of
Tare optional? - 9
What is a type guard in TypeScript?
- 10
What does
Record<string, number>represent? - 11
Given the code below, which line causes a TypeScript compile error? ``
const coords: [number, number] = [10, 20]; // line A coords[0] = 99; // line B coords.push(30); // line C const [x, y] = coords; // line D - 12
What does
keyof Tproduce whenTis{ name: string; age: number }? - 13
Which of these correctly declares a user-defined type guard?
- 14
Given
type Box<T> = T extends string ? "text" : "other", what isBox<string | number>? - 15
What does
Awaited<Promise<Promise<number>>>resolve to? - 16
Which
tsconfigflag causes TypeScript to treatarr[i]asT | undefinedinstead of justT? - 17
How do you extract the element type from an array type
string[]using a conditional type withinfer?
Coding exercises
8 tasksType a User Profile
EasyDefine a type alias called UserProfile with the following fields: id (number), username (string), email (string), age (optional number), and role (a string literal union of "admin", "editor", or "viewer"). Then create a variable me of type UserProfile and assign it a valid value.
// Define your type alias here
// Then create the variable
const me = {
// ...
};
💡 Show hint
Use type UserProfile = { ... } for the alias and "admin" | "editor" | "viewer" for the union literal.
✅ Show solution
type UserProfile = {
id: number;
username: string;
email: string;
age?: number;
role: "admin" | "editor" | "viewer";
};
const me: UserProfile = {
id: 1,
username: "jsmith",
email: "jsmith@example.com",
role: "editor",
};
Generic Identity and Swap
EasyWrite two generic functions:
1. identity<T>(value: T): T — returns its argument unchanged.
2. swap<A, B>(pair: [A, B]): [B, A] — takes a tuple of two elements and returns them in reverse order.
Call each function at least once to demonstrate type inference.
function identity<T>(value: T): T {
// TODO
}
function swap<A, B>(pair: [A, B]): [B, A] {
// TODO
}
💡 Show hint
For swap, destructure the tuple: const [a, b] = pair; then return [b, a].
✅ Show solution
function identity<T>(value: T): T {
return value;
}
function swap<A, B>(pair: [A, B]): [B, A] {
const [a, b] = pair;
return [b, a];
}
// Demonstrate inference
const num = identity(42); // T inferred as number
const str = identity("hello"); // T inferred as string
const swapped = swap(["ts", 100]); // [number, string]
Narrowing with Type Guards
MediumWrite a function formatValue(value: string | number | boolean): string that returns:
- Numbers formatted with two decimal places (e.g. 3.14)
- Booleans as "yes" or "no"
- Strings wrapped in double quotes (e.g. "\"hello\"")
Use typeof type guards to narrow the union inside the function.
function formatValue(value: string | number | boolean): string {
// TODO: use typeof guards
}
💡 Show hint
Check typeof value === "number", then typeof value === "boolean", and fall through to the string case.
✅ Show solution
function formatValue(value: string | number | boolean): string {
if (typeof value === "number") {
return value.toFixed(2);
}
if (typeof value === "boolean") {
return value ? "yes" : "no";
}
return '"' + value + '"';
}
console.log(formatValue(3.14159)); // "3.14"
console.log(formatValue(true)); // "yes"
console.log(formatValue("hello")); // "\"hello\""
Utility Types Workshop
MediumYou have the following interface:
``ts
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
}
Using only built-in utility types, derive:
1. ProductPreview — only id, name, and price fields.
2. ProductUpdate — all fields except id, all optional.
3. ProductCatalog — a Record type mapping a string SKU to a Product`.
Then declare a variable of each derived type with a valid example value.
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
}
// Derive types here
type ProductPreview = /* TODO */;
type ProductUpdate = /* TODO */;
type ProductCatalog = /* TODO */;
💡 Show hint
Use Pick<Product, ...> for ProductPreview, Partial<Omit<Product, "id">> for ProductUpdate, and Record<string, Product> for ProductCatalog.
✅ Show solution
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
}
type ProductPreview = Pick<Product, "id" | "name" | "price">;
type ProductUpdate = Partial<Omit<Product, "id">>;
type ProductCatalog = Record<string, Product>;
// Example values
const preview: ProductPreview = { id: 1, name: "Widget", price: 9.99 };
const update: ProductUpdate = { price: 7.99, inStock: false };
const catalog: ProductCatalog = {
"WDG-001": {
id: 1,
name: "Widget",
price: 9.99,
description: "A small widget",
inStock: true,
},
};
Generic Stack Data Structure
HardImplement a generic Stack<T> class with the following interface:
- push(item: T): void — adds an item to the top.
- pop(): T | undefined — removes and returns the top item, or undefined if empty.
- peek(): T | undefined — returns the top item without removing it.
- readonly size: number — the current number of items.
- isEmpty(): boolean — returns true when the stack has no items.
All internal storage must be private. Demonstrate usage by creating a Stack<string> and a Stack<number>.
class Stack<T> {
// TODO: add private storage
push(item: T): void {
// TODO
}
pop(): T | undefined {
// TODO
}
peek(): T | undefined {
// TODO
}
get size(): number {
// TODO
return 0;
}
isEmpty(): boolean {
// TODO
return true;
}
}
💡 Show hint
Store items in a private items: T[] = []. push calls this.items.push(item), pop calls this.items.pop(), peek returns this.items[this.items.length - 1], and size returns this.items.length.
✅ Show solution
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
// Usage demonstration
const words = new Stack<string>();
words.push("hello");
words.push("world");
console.log(words.peek()); // "world"
console.log(words.size); // 2
console.log(words.pop()); // "world"
console.log(words.size); // 1
const nums = new Stack<number>();
console.log(nums.isEmpty()); // true
nums.push(10);
nums.push(20);
console.log(nums.pop()); // 20
Build a Mapped Type Toolkit
MediumWithout using built-in utility types, implement the following mapped types from scratch:
1. MyPartial<T> — makes all properties optional.
2. MyReadonly<T> — makes all properties readonly.
3. MyNullable<T> — makes all properties accept null in addition to their original type.
Then apply each to this interface and declare one variable per derived type:
``ts
interface Point { x: number; y: number; label: string; }
// Implement from scratch — do not use Partial / Readonly from the stdlib
type MyPartial<T> = {
// TODO
};
type MyReadonly<T> = {
// TODO
};
type MyNullable<T> = {
// TODO
};
interface Point { x: number; y: number; label: string; }
💡 Show hint
Use [K in keyof T]? for optional, readonly [K in keyof T] for readonly, and T[K] | null for nullable values.
✅ Show solution
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type MyNullable<T> = {
[K in keyof T]: T[K] | null;
};
interface Point { x: number; y: number; label: string; }
const p1: MyPartial<Point> = { x: 1 }; // y and label omitted — OK
const p2: MyReadonly<Point> = { x: 0, y: 0, label: "O" }; // cannot reassign any field
const p3: MyNullable<Point> = { x: null, y: 4, label: null };
Discriminated Union Result Type
MediumModel a Result<T, E> type that is either a success or a failure:
- Success: { ok: true; value: T }
- Failure: { ok: false; error: E }
Then write:
1. succeed<T>(value: T): Result<T, never> — wraps a value.
2. fail<E>(error: E): Result<never, E> — wraps an error.
3. unwrap<T, E>(result: Result<T, E>): T — returns the value or throws the error.
Demonstrate usage by calling unwrap on both a success and a failure result (the failure case should throw).
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function succeed<T>(value: T): Result<T, never> {
// TODO
}
function fail<E>(error: E): Result<never, E> {
// TODO
}
function unwrap<T, E>(result: Result<T, E>): T {
// TODO
}
💡 Show hint
Inside unwrap, check result.ok. When true, return result.value. When false, throw result.error (wrap it in an Error if it is a string).
✅ Show solution
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function succeed<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function fail<E>(error: E): Result<never, E> {
return { ok: false, error };
}
function unwrap<T, E>(result: Result<T, E>): T {
if (result.ok) {
return result.value;
}
throw result.error instanceof Error
? result.error
: new Error(String(result.error));
}
// Usage
const good = succeed(42);
console.log(unwrap(good)); // 42
const bad = fail("something went wrong");
try {
unwrap(bad);
} catch (e) {
console.error((e as Error).message); // "something went wrong"
}
Type-Safe Event Emitter with Generics
HardImplement a generic EventEmitter<Events> class where Events is a record mapping event names to their payload types. The class must provide:
- on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void
- off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void
- emit<K extends keyof Events>(event: K, payload: Events[K]): void
Demonstrate by creating an emitter with events { login: { userId: string }; logout: { userId: string }; error: { message: string } } and subscribing to / emitting each event.
type ListenerMap<Events> = {
[K in keyof Events]?: Array<(payload: Events[K]) => void>;
};
class EventEmitter<Events extends Record<string, unknown>> {
private listeners: ListenerMap<Events> = {};
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
// TODO
}
off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
// TODO
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
// TODO
}
}
💡 Show hint
In on, initialise this.listeners[event] as an empty array if it does not exist, then push the listener. In off, filter the listener out. In emit, call each stored listener with the payload.
✅ Show solution
type ListenerMap<Events> = {
[K in keyof Events]?: Array<(payload: Events[K]) => void>;
};
class EventEmitter<Events extends Record<string, unknown>> {
private listeners: ListenerMap<Events> = {};
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
(this.listeners[event] as Array<(p: Events[K]) => void>).push(listener);
}
off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
const arr = this.listeners[event] as Array<(p: Events[K]) => void> | undefined;
if (arr) {
this.listeners[event] = arr.filter((l) => l !== listener) as typeof arr;
}
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const arr = this.listeners[event] as Array<(p: Events[K]) => void> | undefined;
arr?.forEach((l) => l(payload));
}
}
// Usage
type AppEvents = {
login: { userId: string };
logout: { userId: string };
error: { message: string };
};
const emitter = new EventEmitter<AppEvents>();
function onLogin(payload: { userId: string }) {
console.log("User logged in:", payload.userId);
}
emitter.on("login", onLogin);
emitter.on("error", (p) => console.error("Error:", p.message));
emitter.emit("login", { userId: "u_001" }); // "User logged in: u_001"
emitter.emit("error", { message: "Disk full" });
emitter.off("login", onLogin);
emitter.emit("logout", { userId: "u_001" }); // no listeners — silent