Practise TypeScript functions with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning TypeScript for Playwright.
How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with npx tsx program.ts (or compile it with npx tsc).
New to this topic? Read the TypeScript Functions lesson first.
- Beginner programs (1–10)
- Intermediate programs (11–20)
- Real-world QA scenario programs (21–31)
TypeScript Beginner Programs
1. Typed parameters and return typeBeginner
Write add(a: number, b: number): number and call it.
Show solution
function add(a: number, b: number): number {
return a + b;
}
console.log(add(12, 30));
Output
42
Explanation: annotating parameters and the return type documents the contract and lets the compiler check every call.
2. Wrong argument types are compile errorsBeginner
Show that passing a string to a number parameter is rejected.
Show solution
function square(n: number): number {
return n * n;
}
console.log(square(9));
// @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'number'
console.log(square('9'));
Output
81
81
Explanation: the call with a string fails to compile. In plain JavaScript '9' * '9' would silently coerce to 81, hiding the bug.
3. Arrow function with typesBeginner
Write a typed arrow function that builds a full name.
Show solution
const fullName = (first: string, last: string): string => `${first} ${last}`;
console.log(fullName('Asha', 'Sharma'));
Output
Asha Sharma
Explanation: arrow functions take the same parameter and return annotations as regular functions.
4. Optional parameterBeginner
Write greet(name, title?) where the title is optional.
Show solution
function greet(name: string, title?: string): string {
return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}
console.log(greet('Ravi'));
console.log(greet('Ravi', 'Dr.'));
Output
Hello, Ravi
Hello, Dr. Ravi
Explanation: title? has the type string | undefined, so the function checks it before using it.
5. Default parameterBeginner
Write openPage(path, timeout = 30000) and call it with and without a timeout.
Show solution
function openPage(path: string, timeout: number = 30000): string {
return `${path} (${timeout} ms)`;
}
console.log(openPage('/login'));
console.log(openPage('/reports', 60000));
Output
/login (30000 ms)
/reports (60000 ms)
Explanation: a default value makes the parameter optional for callers but always a number inside the function.
6. void return typeBeginner
Write a logger that returns nothing.
Show solution
function logStep(step: string): void {
console.log(`[STEP] ${step}`);
}
logStep('Open login page');
logStep('Submit form');
Output
[STEP] Open login page
[STEP] Submit form
Explanation: void tells callers not to use the return value.
7. Rest parametersBeginner
Write sum(...values: number[]) that adds any number of arguments.
Show solution
function sum(...values: number[]): number {
return values.reduce((total, v) => total + v, 0);
}
console.log(sum(), sum(5), sum(1, 2, 3, 4));
Output
0 5 10
Explanation: a rest parameter collects the arguments into a typed array.
8. Return an objectBeginner
Return min, max and average from a function with an object return type.
Show solution
function stats(values: number[]): { min: number; max: number; avg: number } {
const total = values.reduce((a, b) => a + b, 0);
return { min: Math.min(...values), max: Math.max(...values), avg: total / values.length };
}
console.log(stats([120, 340, 95, 410]));
Output
{ min: 95, max: 410, avg: 241.25 }
Explanation: the return type describes the object, so callers get autocomplete for min, max and avg.
9. Function type aliasBeginner
Declare a Formatter type and implement two formatters.
Show solution
type Formatter = (value: number) => string;
const asCurrency: Formatter = v => `₹${v.toFixed(2)}`;
const asPercent: Formatter = v => `${(v * 100).toFixed(1)}%`;
console.log(asCurrency(1299.5), asPercent(0.925));
Output
₹1299.50 92.5%
Explanation: a function type describes parameters and return type once, and every assigned function must match it.
10. Boolean-returning helperBeginner
Write isValidEmail(value: string): boolean.
Show solution
function isValidEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
console.log(isValidEmail('qa@test.com'), isValidEmail('qa@test'));
Output
true false
Explanation: helpers that return booleans read naturally inside if conditions and assertions.
TypeScript Intermediate Programs
11. Union parameter with narrowingIntermediate
Accept an ID as a number or a string and normalise it.
Show solution
function normaliseId(id: number | string): string {
return typeof id === 'number' ? `ID-${id.toString().padStart(4, '0')}` : id.toUpperCase();
}
console.log(normaliseId(42), normaliseId('id-0042'));
Output
ID-0042 ID-0042
Explanation: narrowing with typeof lets one function handle both types safely.
12. Function overloadsIntermediate
Write parse(value) that returns a number for numeric strings and a boolean for 'true'/'false'.
Show solution
function parse(value: 'true' | 'false'): boolean;
function parse(value: string): number;
function parse(value: string): number | boolean {
if (value === 'true' || value === 'false') return value === 'true';
return Number(value);
}
const flag: boolean = parse('true');
const count: number = parse('42');
console.log(flag, count + 1);
Output
true 43
Explanation: overload signatures give each kind of call its own precise return type.
13. Callback parameterIntermediate
Write forEachResult(results, callback) with a typed callback.
Show solution
type Result = { name: string; passed: boolean };
function forEachResult(results: Result[], callback: (r: Result, index: number) => void): void {
results.forEach((r, i) => callback(r, i));
}
forEachResult([{ name: 'login', passed: true }, { name: 'cart', passed: false }], (r, i) =>
console.log(`${i + 1}. ${r.name}: ${r.passed ? 'pass' : 'fail'}`)
);
Output
1. login: pass
2. cart: fail
Explanation: the callback’s type gives the arrow function’s parameters their types automatically.
14. Higher-order function returning a functionIntermediate
Write makeMultiplier(factor) returning a typed function.
Show solution
function makeMultiplier(factor: number): (n: number) => number {
return n => n * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(21), triple(7));
Output
42 21
Explanation: the return type (n: number) => number describes the function being returned.
15. Closure with private stateIntermediate
Create a typed counter with increment and reset.
Show solution
function createCounter(start: number = 0): { next: () => number; reset: () => void } {
let count = start;
return {
next: () => ++count,
reset: () => { count = start; }
};
}
const counter = createCounter(10);
console.log(counter.next(), counter.next());
counter.reset();
console.log(counter.next());
Output
11 12
11
Explanation: count is captured by the closure and can only change through the returned methods.
16. Generic identity-style helperIntermediate
Write first<T>(items: T[]): T | undefined.
Show solution
function first<T>(items: T[]): T | undefined {
return items[0];
}
const browser = first(['chromium', 'firefox']);
const score = first([98, 87]);
const none = first<string>([]);
console.log(browser?.toUpperCase(), score?.toFixed(1), none ?? 'empty');
Output
CHROMIUM 98.0 empty
Explanation: the type parameter T is inferred from the argument, so the return type matches the array’s element type.
17. Destructured parameter with defaultsIntermediate
Accept an options object with defaults.
Show solution
type Options = { retries?: number; timeout?: number; headless?: boolean };
function describe({ retries = 0, timeout = 30000, headless = true }: Options = {}): string {
return `retries=${retries}, timeout=${timeout}, headless=${headless}`;
}
console.log(describe());
console.log(describe({ retries: 2, headless: false }));
Output
retries=0, timeout=30000, headless=true
retries=2, timeout=30000, headless=false
Explanation: destructuring in the parameter list with defaults keeps calls short when many settings are optional.
18. Recursion with typesIntermediate
Flatten nested arrays of numbers recursively.
Show solution
type Nested = number | Nested[];
function flatten(value: Nested): number[] {
return Array.isArray(value) ? value.flatMap(flatten) : [value];
}
console.log(flatten([1, [2, [3, [4]]], 5]).join(','));
Output
1,2,3,4,5
Explanation: a recursive type alias describes arbitrarily nested data, and the recursive function mirrors it.
19. Assertion functionIntermediate
Write assertNumber(value) that narrows unknown after the call.
Show solution
function assertNumber(value: unknown, name: string): asserts value is number {
if (typeof value !== 'number' || Number.isNaN(value)) throw new Error(`${name} must be a number`);
}
const raw: unknown = JSON.parse('{"workers": 4}').workers;
assertNumber(raw, 'workers');
console.log(raw * 2);
Output
8
Explanation: after assertNumber returns, TypeScript treats raw as a number.
20. Async function return typeIntermediate
Write an async function that returns Promise<string>.
Show solution
async function fetchTitle(path: string): Promise<string> {
await new Promise(resolve => setTimeout(resolve, 10));
return `Title of ${path}`;
}
const title: string = await fetchTitle('/home');
console.log(title);
Output
Title of /home
Explanation: an async function always returns a Promise. Awaiting it gives the inner type.
TypeScript Real-World QA Scenario Programs
21. Typed assertion helperReal-world
Write expectEqual<T>(actual, expected, label) that reports PASS or FAIL.
Show solution
function expectEqual<T>(actual: T, expected: T, label: string): boolean {
const ok = actual === expected;
console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${ok ? '' : ` (expected ${String(expected)}, got ${String(actual)})`}`);
return ok;
}
expectEqual('Shop'.length, 4, 'title length');
expectEqual('cart'.toUpperCase(), 'CART', 'upper case');
expectEqual(0.1 + 0.2, 0.3, 'float sum');
Output
PASS title length
PASS upper case
FAIL float sum (expected 0.3, got 0.30000000000000004)
Explanation: the generic T makes actual and expected the same type, like Playwright’s expect(actual).toBe(expected).
22. URL builderReal-world
Write buildUrl(base, path, params?) with a typed query object.
Show solution
function buildUrl(base: string, path: string, params: Record<string, string | number> = {}): string {
const url = new URL(path, base);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
return url.toString();
}
console.log(buildUrl('https://shop.test', '/search', { q: 'mouse', page: 2 }));
Output
https://shop.test/search?q=mouse&page=2
Explanation: a Record type keeps query values to strings or numbers, and URL encodes them.
23. Retry wrapper with genericsReal-world
Write retry<T>(fn, attempts) that returns the result of a successful attempt.
Show solution
function retry<T>(fn: (attempt: number) => T, attempts: number): T {
let lastError: unknown;
for (let i = 1; i <= attempts; i++) {
try {
return fn(i);
} catch (e) {
lastError = e;
console.log(`attempt ${i} failed`);
}
}
throw lastError;
}
const value = retry(attempt => {
if (attempt < 3) throw new Error('not ready');
return `ready on attempt ${attempt}`;
}, 5);
console.log(value);
Output
attempt 1 failed
attempt 2 failed
ready on attempt 3
Explanation: the generic return type means retry returns exactly what the wrapped function returns.
24. Async retry with delayReal-world
Write an async retry helper for flaky API calls.
Show solution
const sleep = (ms: number): Promise<void> => new Promise(r => setTimeout(r, ms));
async function retryAsync<T>(fn: () => Promise<T>, attempts: number, delayMs: number): Promise<T> {
for (let i = 1; ; i++) {
try {
return await fn();
} catch (e) {
if (i >= attempts) throw e;
console.log(`retry ${i}: ${e instanceof Error ? e.message : e}`);
await sleep(delayMs);
}
}
}
let calls = 0;
const data = await retryAsync(async () => {
if (++calls < 3) throw new Error('503');
return { items: 3 };
}, 4, 10);
console.log('items:', data.items);
Output
retry 1: 503
retry 2: 503
items: 3
Explanation: return await inside try lets rejections reach catch. The generic T keeps the result typed.
25. Duration formatterReal-world
Format milliseconds into a readable duration.
Show solution
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms} ms`;
if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`;
const m = Math.floor(ms / 60_000);
const s = Math.round((ms % 60_000) / 1000);
return `${m}m ${s}s`;
}
console.log([850, 2500, 125_000].map(formatDuration).join(' | '));
Output
850 ms | 2.5 s | 2m 5s
Explanation: one typed formatter keeps reports consistent. Numeric separators such as 60_000 improve readability.
26. Validation returning a result typeReal-world
Validate a signup form and return a typed result.
Show solution
type Signup = { email: string; password: string; age: number };
type Validation = { valid: true } | { valid: false; errors: string[] };
function validate(form: Signup): Validation {
const errors: string[] = [];
if (!form.email.includes('@')) errors.push('invalid email');
if (form.password.length < 8) errors.push('password too short');
if (form.age < 18) errors.push('must be 18+');
return errors.length ? { valid: false, errors } : { valid: true };
}
for (const f of [{ email: 'qa@test.com', password: 'Secret123', age: 30 }, { email: 'qa', password: '123', age: 16 }]) {
const r = validate(f);
console.log(r.valid ? 'valid' : r.errors.join(', '));
}
Output
valid
invalid email, password too short, must be 18+
Explanation: the union result type forces callers to check valid before reading errors.
27. Data builder with PartialReal-world
Write buildUser(overrides: Partial<User>) for test data.
Show solution
interface User { name: string; email: string; role: 'admin' | 'viewer'; active: boolean }
function buildUser(overrides: Partial<User> = {}): User {
return { name: 'QA User', email: 'qa.user@example.com', role: 'viewer', active: true, ...overrides };
}
const admin = buildUser({ role: 'admin', name: 'Admin' });
console.log(admin.name, admin.role, admin.active);
Output
Admin admin true
Explanation: Partial<User> lets each test override only the fields it cares about, while the result is always a complete User.
28. Page-object style functionsReal-world
Write a typed factory that returns login page actions.
Show solution
interface LoginPage {
fill(field: 'email' | 'password', value: string): LoginPage;
submit(): LoginPage;
log(): string;
}
function createLoginPage(): LoginPage {
const actions: string[] = [];
const page: LoginPage = {
fill(field, value) { actions.push(`fill ${field}=${field === 'password' ? '***' : value}`); return page; },
submit() { actions.push('submit'); return page; },
log() { return actions.join(' -> '); }
};
return page;
}
console.log(createLoginPage().fill('email', 'qa@test.com').fill('password', 'secret').submit().log());
Output
fill email=qa@test.com -> fill password=*** -> submit
Explanation: the interface types the chainable methods, and field only accepts the two known field names.
29. Debounce with genericsReal-world
Write a typed debounce for a search handler.
Show solution
function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): (...args: A) => void {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: A) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
const search = debounce((term: string) => console.log('search:', term), 20);
search('p'); search('pla'); search('playwright');
await new Promise(r => setTimeout(r, 50));
Output
search: playwright
Explanation: the generic A preserves the wrapped function’s parameter types, so search(123) would be a compile error.
30. Compose functionsReal-world
Combine string transformers into a slug function.
Show solution
type Transform = (s: string) => string;
const pipe = (...fns: Transform[]): Transform => input => fns.reduce((acc, fn) => fn(acc), input);
const slugify = pipe(s => s.trim(), s => s.toLowerCase(), s => s.replace(/[^a-z0-9]+/g, '-'), s => s.replace(/^-|-$/g, ''));
console.log(slugify(' Playwright: Getting Started! '));
Output
playwright-getting-started
Explanation: small typed transforms combine into a pipeline that is easy to test step by step.
31. Typed event handlers mapReal-world
Map test events to typed handler functions.
Show solution
type TestEvent = 'start' | 'pass' | 'fail';
const handlers: Record<TestEvent, (name: string) => string> = {
start: n => `▶ ${n}`,
pass: n => `✔ ${n}`,
fail: n => `✘ ${n}`
};
const events: [TestEvent, string][] = [['start', 'login'], ['pass', 'login'], ['start', 'cart'], ['fail', 'cart']];
for (const [event, name] of events) console.log(handlers[event](name));
Output
▶ login
✔ login
▶ cart
✘ cart
Explanation: a Record of functions requires a handler for every event, and each handler has the same signature.