TypeScript Conditions Practice Programs with Solutions (30 Programs)

Practise TypeScript conditions and narrowing with 30 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 Conditions lesson first.

TypeScript Beginner Programs

1. Positive, negative or zeroBeginner

Write sign(n: number) that returns 'positive', 'negative' or 'zero'.

Show solution
type Sign = 'positive' | 'negative' | 'zero';
function sign(n: number): Sign {
  if (n > 0) return 'positive';
  if (n < 0) return 'negative';
  return 'zero';
}
console.log(sign(7), sign(-2), sign(0));

Output

positive negative zero

Explanation: the return type is a literal union, so every branch must return one of the allowed strings.

2. Pass or failBeginner

Return 'PASS' when a score is at least 60, otherwise 'FAIL'.

Show solution
function result(score: number): 'PASS' | 'FAIL' {
  if (score >= 60) {
    return 'PASS';
  } else {
    return 'FAIL';
  }
}
console.log(result(72), result(45));

Output

PASS FAIL

Explanation: if/else picks one of two outcomes. The return type documents both possibilities.

3. Grade bandsBeginner

Convert marks into grades A, B, C or F with an else if chain.

Show solution
type Grade = 'A' | 'B' | 'C' | 'F';
function grade(marks: number): Grade {
  if (marks >= 90) return 'A';
  else if (marks >= 75) return 'B';
  else if (marks >= 60) return 'C';
  else return 'F';
}
console.log([95, 80, 61, 30].map(grade).join(' '));

Output

A B C F

Explanation: checking the highest threshold first ensures each value lands in the right band.

4. Leap yearBeginner

Write isLeapYear(year: number): boolean and test three years.

Show solution
function isLeapYear(year: number): boolean {
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
for (const y of [2024, 1900, 2000]) console.log(y, isLeapYear(y));

Output

2024 true
1900 false
2000 true

Explanation: the whole condition is a boolean expression, so it can be returned directly.

5. switch on a string unionBeginner

Map a day name to a working-day flag with switch.

Show solution
type Day = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun';
function isWorkingDay(day: Day): boolean {
  switch (day) {
    case 'Sat':
    case 'Sun':
      return false;
    default:
      return true;
  }
}
console.log(isWorkingDay('Mon'), isWorkingDay('Sun'));

Output

true false

Explanation: TypeScript checks that each case is a member of the Day union, so typos are caught.

6. Ternary with typesBeginner

Choose a label with a ternary and store it in a typed variable.

Show solution
const itemCount: number = 1;
const label: string = itemCount === 1 ? 'item' : 'items';
console.log(`${itemCount} ${label}`);

Output

1 item

Explanation: both branches return strings, so the result type is string.

7. Handle a nullable valueBeginner

Print a greeting when a name is present, otherwise a default message.

Show solution
function greet(name: string | null): string {
  if (name === null) return 'Hello, guest';
  return `Hello, ${name.toUpperCase()}`;
}
console.log(greet('asha'));
console.log(greet(null));

Output

Hello, ASHA
Hello, guest

Explanation: after the null check, TypeScript narrows name to string, so toUpperCase is allowed.

8. Optional parameter checkBeginner

Use a default when an optional parameter is missing.

Show solution
function openPage(path: string, timeout?: number): string {
  if (timeout === undefined) return `${path} (default timeout)`;
  return `${path} (${timeout} ms)`;
}
console.log(openPage('/login'));
console.log(openPage('/reports', 60000));

Output

/login (default timeout)
/reports (60000 ms)

Explanation: an optional parameter has type number | undefined, so you check for undefined before using it.

9. Truthiness narrowingBeginner

Print the length of a string only when it is non-empty.

Show solution
function describe(text?: string): string {
  if (text) return `${text.length} characters`;
  return 'empty or missing';
}
console.log(describe('Playwright'), '|', describe(''), '|', describe());

Output

10 characters | empty or missing | empty or missing

Explanation: if (text) removes undefined and also skips the empty string, which is falsy.

10. Largest of threeBeginner

Return the largest of three numbers with if conditions.

Show solution
function largest(a: number, b: number, c: number): number {
  if (a >= b && a >= c) return a;
  if (b >= c) return b;
  return c;
}
console.log(largest(12, 45, 30), largest(9, 3, 9));

Output

45 9

Explanation: once a is ruled out, only b and c remain to be compared.

TypeScript Intermediate Programs

11. typeof narrowingIntermediate

Format an input that may be a number, string or boolean.

Show solution
function format(value: number | string | boolean): string {
  if (typeof value === 'number') return value.toFixed(1);
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
  return value.trim();
}
console.log(format(3.14159), format(true), format('  ok  '));

Output

3.1 yes ok

Explanation: each typeof check narrows the union, and the final return knows only string is left.

12. Discriminated union with ifIntermediate

Handle API responses that are either success or error.

Show solution
type Res = { status: 'ok'; data: string[] } | { status: 'error'; message: string };
function handle(res: Res): string {
  if (res.status === 'ok') return `Loaded ${res.data.length} rows`;
  return `Error: ${res.message}`;
}
console.log(handle({ status: 'ok', data: ['a', 'b'] }));
console.log(handle({ status: 'error', message: 'Unauthorized' }));

Output

Loaded 2 rows
Error: Unauthorized

Explanation: checking the shared status field narrows to the matching member of the union.

13. Exhaustive switch with neverIntermediate

Map test outcomes to symbols and make the compiler check every case.

Show solution
type Outcome = 'passed' | 'failed' | 'skipped' | 'flaky';
function symbol(o: Outcome): string {
  switch (o) {
    case 'passed': return '✔';
    case 'failed': return '✘';
    case 'skipped': return '○';
    case 'flaky': return '≈';
    default: {
      const unhandled: never = o;
      throw new Error(`Unhandled outcome: ${unhandled}`);
    }
  }
}
console.log((['passed', 'flaky', 'failed'] as Outcome[]).map(symbol).join(' '));

Output

✔ ≈ ✘

Explanation: adding a new outcome without a case makes the never assignment fail to compile.

14. in operator narrowingIntermediate

Tell a Card payment from a Upi payment.

Show solution
type Card = { cardNumber: string; expiry: string };
type Upi = { upiId: string };
function paymentLabel(p: Card | Upi): string {
  if ('cardNumber' in p) return `Card ending ${p.cardNumber.slice(-4)}`;
  return `UPI ${p.upiId}`;
}
console.log(paymentLabel({ cardNumber: '4111111111111234', expiry: '12/28' }));
console.log(paymentLabel({ upiId: 'qa@bank' }));

Output

Card ending 1234
UPI qa@bank

Explanation: checking for a property with in narrows a union of object types.

15. instanceof narrowingIntermediate

Handle a caught error safely, whatever was thrown.

Show solution
function message(err: unknown): string {
  if (err instanceof Error) return err.message;
  if (typeof err === 'string') return err;
  return 'Unknown error';
}
console.log(message(new Error('Timeout')), '|', message('Network down'), '|', message(42));

Output

Timeout | Network down | Unknown error

Explanation: in strict mode a caught error is unknown, so narrow it before reading message.

16. Custom type guardIntermediate

Write isNumberArray and use it in a condition.

Show solution
function isNumberArray(value: unknown): value is number[] {
  return Array.isArray(value) && value.every(v => typeof v === 'number');
}
for (const input of [[1, 2, 3], ['1', 2], 'x'] as unknown[]) {
  if (isNumberArray(input)) console.log('sum', input.reduce((a, b) => a + b, 0));
  else console.log('not a number array');
}

Output

sum 6
not a number array
not a number array

Explanation: the value is number[] return type narrows the variable in the if branch.

17. Equality narrowingIntermediate

Narrow two union-typed values by comparing them.

Show solution
function compare(a: string | number, b: string | boolean): string {
  if (a === b) return `both are the string "${a.toUpperCase()}"`;
  return 'different';
}
console.log(compare('qa', 'qa'));
console.log(compare(1, true));

Output

both are the string "QA"
different

Explanation: if a === b, both must be strings (the only shared type), so toUpperCase is allowed.

18. Lookup object instead of switchIntermediate

Map HTTP status codes to messages with a typed record and a default.

Show solution
const STATUS_TEXT: Record<number, string> = { 200: 'OK', 201: 'Created', 404: 'Not Found', 500: 'Server Error' };
function statusText(code: number): string {
  return STATUS_TEXT[code] ?? 'Unknown';
}
console.log([200, 404, 418].map(statusText).join(', '));

Output

OK, Not Found, Unknown

Explanation: a lookup table replaces long switch statements. ?? supplies the default.

19. Guard clauses with typesIntermediate

Validate an age value with early returns.

Show solution
function validateAge(age: unknown): string {
  if (typeof age !== 'number') return 'Age must be a number';
  if (!Number.isInteger(age)) return 'Age must be a whole number';
  if (age < 18) return 'Must be 18 or older';
  return 'Valid';
}
console.log([25, '25', 17.5, 16].map(validateAge).join(' | '));

Output

Valid | Age must be a number | Age must be a whole number | Must be 18 or older

Explanation: after the first guard, age is a number for all the following checks.

20. Assertion functionIntermediate

Write an assertDefined function that narrows a value after it runs.

Show solution
function assertDefined<T>(value: T | undefined, name: string): asserts value is T {
  if (value === undefined) throw new Error(`${name} is required`);
}
const env: Record<string, string | undefined> = { BASE_URL: 'https://qa.app.test' };
const baseUrl = env.BASE_URL;
assertDefined(baseUrl, 'BASE_URL');
console.log(baseUrl.toUpperCase());
try {
  assertDefined(env.TOKEN, 'TOKEN');
} catch (e) {
  console.log(e instanceof Error ? e.message : e);
}

Output

HTTPS://QA.APP.TEST
TOKEN is required

Explanation: asserts value is T tells TypeScript the value is defined after the call, like expect(...).toBeDefined() combined with type narrowing.

TypeScript Real-World QA Scenario Programs

21. Shipping cost rulesReal-world

Calculate shipping with typed thresholds, then test the boundary values.

Show solution
const FREE_ABOVE = 999, MID_FROM = 500;
function shipping(total: number): number {
  if (total > FREE_ABOVE) return 0;
  if (total >= MID_FROM) return 49;
  return 99;
}
for (const t of [499, 500, 999, 1000]) console.log(t, shipping(t));

Output

499 99
500 49
999 49
1000 0

Explanation: boundary values (499, 500, 999, 1000) are where off-by-one bugs appear, so test them explicitly.

22. Environment selectionReal-world

Choose a base URL from an environment name with an exhaustive switch.

Show solution
type Env = 'dev' | 'qa' | 'prod';
function baseUrl(env: Env): string {
  switch (env) {
    case 'dev': return 'http://localhost:3000';
    case 'qa': return 'https://qa.shop.test';
    case 'prod': return 'https://shop.test';
  }
}
console.log(baseUrl('qa'));

Output

https://qa.shop.test

Explanation: because every Env member returns, TypeScript knows the function never falls through without a value.

23. Password strength rulesReal-world

Return the first failing password rule, or 'Strong'.

Show solution
function passwordIssue(pw: string): string {
  if (pw.length < 8) return 'Too short';
  if (!/[A-Z]/.test(pw)) return 'Needs an uppercase letter';
  if (!/\d/.test(pw)) return 'Needs a digit';
  if (!/[^A-Za-z0-9]/.test(pw)) return 'Needs a symbol';
  return 'Strong';
}
for (const pw of ['abc', 'abcdefgh', 'Abcdefgh', 'Abcdefg1', 'Abcdef1!']) console.log(pw, '->', passwordIssue(pw));

Output

abc -> Too short
abcdefgh -> Needs an uppercase letter
Abcdefgh -> Needs a digit
Abcdefg1 -> Needs a symbol
Abcdef1! -> Strong

Explanation: ordered guard clauses mirror the order in which a form shows validation messages.

24. Classify an API responseReal-world

Classify typed responses into success, client error or server error.

Show solution
type ApiResponse = { status: number; body?: { items: string[] } };
function classify(r: ApiResponse): string {
  if (r.status >= 500) return 'server error';
  if (r.status >= 400) return 'client error';
  if (!r.body || r.body.items.length === 0) return 'success (empty)';
  const n = r.body.items.length;
  return `success (${n} item${n === 1 ? '' : 's'})`;
}
const responses: ApiResponse[] = [{ status: 200, body: { items: ['a'] } }, { status: 200 }, { status: 404 }, { status: 503 }];
for (const r of responses) console.log(r.status, classify(r));

Output

200 success (1 item)
200 success (empty)
404 client error
503 server error

Explanation: checking error statuses first and body before items keeps every access type-safe.

25. Feature access by planReal-world

Decide whether a plan can use a feature.

Show solution
type Plan = 'free' | 'pro' | 'enterprise';
type Feature = 'reports' | 'sso' | 'export';
function canUse(plan: Plan, feature: Feature): boolean {
  if (feature === 'sso') return plan === 'enterprise';
  if (feature === 'reports') return plan !== 'free';
  return true;
}
for (const plan of ['free', 'pro', 'enterprise'] as Plan[]) {
  console.log(plan, canUse(plan, 'reports'), canUse(plan, 'sso'), canUse(plan, 'export'));
}

Output

free false false true
pro true false true
enterprise true true true

Explanation: literal unions for both inputs make invalid plan or feature names a compile error.

26. Test priority triageReal-world

Assign P1, P2 or P3 to typed test cases.

Show solution
type TestCase = { id: string; area: string; blocker: boolean; regression: boolean };
type Priority = 'P1' | 'P2' | 'P3';
function priority(t: TestCase): Priority {
  if (t.area === 'checkout' || t.blocker) return 'P1';
  if (t.regression) return 'P2';
  return 'P3';
}
const tests: TestCase[] = [
  { id: 'TC1', area: 'checkout', blocker: false, regression: true },
  { id: 'TC2', area: 'search', blocker: false, regression: true },
  { id: 'TC3', area: 'help', blocker: false, regression: false }
];
console.log(tests.map(t => `${t.id}:${priority(t)}`).join(' '));

Output

TC1:P1 TC2:P2 TC3:P3

Explanation: the rule reads like the written triage policy, and the types document the inputs.

27. Flaky test classificationReal-world

Classify a test from its pass count over a number of runs.

Show solution
type Stability = 'stable' | 'flaky' | 'broken';
function stability(passes: number, runs: number): Stability {
  if (passes === runs) return 'stable';
  if (passes === 0) return 'broken';
  return 'flaky';
}
for (const [p, r] of [[10, 10], [7, 10], [0, 10]] as [number, number][]) console.log(`${p}/${r}`, stability(p, r));

Output

10/10 stable
7/10 flaky
0/10 broken

Explanation: the typed tuple array makes it clear each entry is a pair of numbers.

28. Retry decisionReal-world

Retry only on server errors or 429, while attempts remain.

Show solution
type Attempt = { status: number; attempt: number };
const MAX_ATTEMPTS = 3;
function shouldRetry({ status, attempt }: Attempt): boolean {
  if (attempt >= MAX_ATTEMPTS) return false;
  return status >= 500 || status === 429;
}
const cases: Attempt[] = [{ status: 503, attempt: 1 }, { status: 404, attempt: 1 }, { status: 429, attempt: 3 }];
for (const c of cases) console.log(c.status, c.attempt, shouldRetry(c));

Output

503 1 true
404 1 false
429 3 false

Explanation: a guard clause handles the attempt limit first, so the status rule stays simple.

29. Weekend scheduling checkReal-world

Skip weekend dates when scheduling a nightly run, using UTC days.

Show solution
function shouldRun(isoDate: string): boolean {
  const day = new Date(`${isoDate}T00:00:00Z`).getUTCDay();
  return day !== 0 && day !== 6;
}
for (const d of ['2026-11-06', '2026-11-07', '2026-11-09']) console.log(d, shouldRun(d) ? 'run' : 'skip');

Output

2026-11-06 run
2026-11-07 skip
2026-11-09 run

Explanation: getUTCDay() returns 0 for Sunday and 6 for Saturday, independent of the machine’s time zone.

30. Locator choice by available attributesReal-world

Pick the best locator strategy from what an element provides.

Show solution
type ElementInfo = { role?: string; name?: string; testId?: string; css: string };
function bestLocator(el: ElementInfo): string {
  if (el.role && el.name) return `getByRole('${el.role}', { name: '${el.name}' })`;
  if (el.testId) return `getByTestId('${el.testId}')`;
  return `locator('${el.css}')`;
}
console.log(bestLocator({ role: 'button', name: 'Pay', css: '.btn' }));
console.log(bestLocator({ testId: 'cart-total', css: '#total' }));
console.log(bestLocator({ css: 'div > span:nth-child(2)' }));

Output

getByRole('button', { name: 'Pay' })
getByTestId('cart-total')
locator('div > span:nth-child(2)')

Explanation: conditions encode Playwright’s locator priority: role and name first, then test ID, then CSS.

Keep Practising

Leave a Comment