TypeScript Functions Practice Programs with Solutions (31 Programs)

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.

Advertisement

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.

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&lt;string&gt;.

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&lt;T&gt;(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&lt;T&gt;(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&lt;User&gt;) 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&lt;User&gt; 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.

Keep Practising

Leave a Comment