TypeScript Operators Practice Programs with Solutions (30 Programs)

Practise TypeScript operators 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 Operators lesson first.

TypeScript Beginner Programs

1. Typed arithmeticBeginner

Write calculate(a, b) that returns the sum, difference, product and remainder as an object.

Show solution
function calculate(a: number, b: number): { sum: number; diff: number; product: number; rem: number } {
  return { sum: a + b, diff: a - b, product: a * b, rem: a % b };
}
console.log(calculate(17, 5));

Output

{ sum: 22, diff: 12, product: 85, rem: 2 }

Explanation: typed parameters guarantee both operands are numbers, so + always adds instead of concatenating.

2. Prevent string concatenation bugsBeginner

Show that a typed function rejects a numeric string argument.

Show solution
function addPrices(a: number, b: number): number {
  return a + b;
}
console.log(addPrices(499, 1299));
// @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'number'
console.log(addPrices('499', 1299));

Output

1798
4991299

Explanation: in JavaScript '499' + 1299 gives '4991299'. TypeScript catches the wrong argument type before the code runs.

3. Comparison operators return booleanBeginner

Compare scores and store the results in boolean variables.

Show solution
const score: number = 72;
const passMark: number = 60;
const passed: boolean = score >= passMark;
const perfect: boolean = score === 100;
console.log(passed, perfect);

Output

true false

Explanation: comparison operators always produce boolean values.

4. Comparing unrelated types is an errorBeginner

Show that comparing a number with a string literal is flagged.

Show solution
const status: number = 200;
// @ts-expect-error: This comparison appears to be unintentional because the types have no overlap
const same = status === '200';
console.log(same, status === Number('200'));

Output

false true

Explanation: TypeScript reports comparisons that can never be true, a common bug when API values are strings.

5. Logical operators with booleansBeginner

Combine boolean flags with &&, || and !.

Show solution
const isLoggedIn: boolean = true;
const isAdmin: boolean = false;
const canEdit: boolean = isLoggedIn && isAdmin;
const canView: boolean = isLoggedIn || isAdmin;
console.log(canEdit, canView, !isAdmin);

Output

false true true

Explanation: logical operators combine conditions. With boolean operands they return booleans.

6. Increment and compound assignmentBeginner

Update a typed counter with ++, += and *=.

Show solution
let retries: number = 0;
retries++;
retries += 2;
retries *= 10;
console.log(retries);

Output

30

Explanation: compound operators keep the variable’s type, here number.

7. Ternary with typed resultBeginner

Return a literal union from a ternary expression.

Show solution
type Verdict = 'PASS' | 'FAIL';
const verdict = (score: number): Verdict => (score >= 60 ? 'PASS' : 'FAIL');
console.log(verdict(75), verdict(40));

Output

PASS FAIL

Explanation: both branches return members of the Verdict union, so the function’s return type is satisfied.

8. Nullish coalescing with typed defaultsBeginner

Use ?? to provide a default for an optional timeout.

Show solution
function getTimeout(custom?: number): number {
  return custom ?? 30000;
}
console.log(getTimeout(), getTimeout(0), getTimeout(5000));

Output

30000 0 5000

Explanation: ?? falls back only for undefined or null, so a valid 0 is kept. The result type is number.

9. Optional chainingBeginner

Safely read a nested optional property.

Show solution
type User = { name: string; address?: { city?: string } };
const users: User[] = [{ name: 'Asha', address: { city: 'Delhi' } }, { name: 'Ravi' }];
for (const u of users) console.log(u.name, u.address?.city ?? 'unknown');

Output

Asha Delhi
Ravi unknown

Explanation: ?. returns undefined when a link in the chain is missing, and TypeScript types the result as string | undefined.

10. Exponent and Math operationsBeginner

Calculate compound growth with **.

Show solution
const principal: number = 1000;
const rate: number = 0.1;
const years: number = 3;
const amount: number = principal * (1 + rate) ** years;
console.log(amount.toFixed(2));

Output

1331.00

Explanation: ** raises a number to a power, and the result type is number.

TypeScript Intermediate Programs

11. typeof narrowingIntermediate

Format a value that can be a number or a string.

Show solution
function format(value: number | string): string {
  if (typeof value === 'number') return value.toFixed(2);
  return value.trim().toUpperCase();
}
console.log(format(3.14159));
console.log(format('  passed '));

Output

3.14
PASSED

Explanation: after typeof value === 'number', TypeScript knows value is a number. In the other branch it must be a string.

12. in operator narrowingIntermediate

Distinguish two object shapes with the in operator.

Show solution
type ApiError = { error: string };
type ApiData = { data: number[] };
function describe(res: ApiError | ApiData): string {
  return 'error' in res ? `Error: ${res.error}` : `Data: ${res.data.length} items`;
}
console.log(describe({ error: 'Unauthorized' }));
console.log(describe({ data: [1, 2, 3] }));

Output

Error: Unauthorized
Data: 3 items

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

13. instanceof narrowingIntermediate

Handle a Date or a timestamp number.

Show solution
function toIso(value: Date | number): string {
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}
console.log(toIso(new Date('2026-01-01T00:00:00Z')));
console.log(toIso(0));

Output

2026-01-01T00:00:00.000Z
1970-01-01T00:00:00.000Z

Explanation: instanceof narrows to a class type, so Date methods are allowed in that branch.

14. Non-null assertionIntermediate

Use ! when you know a value is not undefined, and compare it with a safe check.

Show solution
const roles = new Map<string, string>([['asha', 'admin']]);
const role: string = roles.get('asha')!;
console.log(role);
const maybe = roles.get('ravi');
console.log(maybe !== undefined ? maybe : 'no role');

Output

admin
no role

Explanation: ! removes undefined from the type without a runtime check. Prefer a real check when the value might be missing.

15. keyof operatorIntermediate

Read a property of a config object using a key typed with keyof.

Show solution
const config = { browser: 'chromium', workers: 4, headless: true };
type ConfigKey = keyof typeof config;
function read(key: ConfigKey) {
  return config[key];
}
console.log(read('workers'), read('browser'));
// @ts-expect-error: '"timeout"' is not assignable to parameter of type 'ConfigKey'
read('timeout');

Output

4 chromium

Explanation: keyof creates a union of property names, so only existing keys can be passed.

16. typeof type operatorIntermediate

Create a type from an existing value with typeof.

Show solution
const defaultUser = { name: 'qa', role: 'viewer' as 'viewer' | 'admin' };
type User = typeof defaultUser;
const admin: User = { name: 'boss', role: 'admin' };
console.log(admin.role);

Output

admin

Explanation: typeof value in a type position reuses the value’s type, so you do not repeat it by hand.

17. Indexed access typesIntermediate

Get the type of a nested property with indexed access.

Show solution
type Order = { id: number; customer: { name: string; tier: 'gold' | 'silver' } };
type Tier = Order['customer']['tier'];
const tiers: Tier[] = ['gold', 'silver', 'gold'];
console.log(tiers.filter(t => t === 'gold').length);

Output

2

Explanation: Type['prop'] looks up a property’s type, so related types stay in sync automatically.

18. satisfies operatorIntermediate

Check a config against a type while keeping its literal values.

Show solution
type Env = 'qa' | 'prod';
const urls = {
  qa: 'https://qa.app.test',
  prod: 'https://app.test'
} satisfies Record<Env, string>;
console.log(urls.qa.startsWith('https'), Object.keys(urls).length);

Output

true 2

Explanation: satisfies validates the shape without widening the type, so the compiler still knows the exact keys.

19. as const with operatorsIntermediate

Use a literal-typed array with includes and a type guard.

Show solution
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number];
const isRole = (value: string): value is Role => (ROLES as readonly string[]).includes(value);
for (const v of ['editor', 'guest']) console.log(v, isRole(v));

Output

editor true
guest false

Explanation: widening the array to readonly string[] lets includes accept any string, while the type guard narrows the result.

20. Bitwise flags with an enumIntermediate

Combine permission flags with | and test them with &amp;.

Show solution
enum Permission { Read = 1, Write = 2, Delete = 4 }
const userPerms: number = Permission.Read | Permission.Write;
const can = (p: Permission): boolean => (userPerms & p) !== 0;
console.log(can(Permission.Read), can(Permission.Write), can(Permission.Delete));

Output

true true false

Explanation: numeric enum values are numbers, so bitwise operators combine and test flags.

TypeScript Real-World QA Scenario Programs

21. Cart total with taxReal-world

Write a typed function that calculates a cart total with tax.

Show solution
type CartItem = { name: string; price: number; qty: number };
function cartTotal(items: CartItem[], taxRate: number): number {
  const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  return Math.round(subtotal * (1 + taxRate) * 100) / 100;
}
console.log(cartTotal([{ name: 'Mouse', price: 599, qty: 2 }, { name: 'Pad', price: 299, qty: 1 }], 0.18));

Output

1766.46

Explanation: typed items guarantee price and qty are numbers, so the arithmetic is safe.

22. Pass rate with typed resultReal-world

Calculate a pass rate and return a typed status.

Show solution
type Health = 'green' | 'amber' | 'red';
function health(passed: number, total: number): { rate: number; status: Health } {
  const rate = total === 0 ? 0 : (passed / total) * 100;
  const status: Health = rate >= 95 ? 'green' : rate >= 80 ? 'amber' : 'red';
  return { rate: Number(rate.toFixed(1)), status };
}
console.log(health(97, 100), health(41, 50), health(5, 10));

Output

{ rate: 97, status: 'green' } { rate: 82, status: 'amber' } { rate: 50, status: 'red' }

Explanation: the return type documents the shape, and the guard total === 0 avoids dividing by zero.

23. Response time SLA checkReal-world

Check whether response times are within an SLA range.

Show solution
const SLA = { min: 0, max: 800 } as const;
const within = (ms: number): boolean => ms >= SLA.min && ms <= SLA.max;
const timings: number[] = [120, 950, 640];
for (const t of timings) console.log(`${t} ms`, within(t) ? 'OK' : 'BREACH');

Output

120 ms OK
950 ms BREACH
640 ms OK

Explanation: &amp;&amp; combines the two boundary checks, and the typed array guarantees numeric input.

24. Optional chaining on an API responseReal-world

Safely read the first item’s name from a response that may be empty.

Show solution
type Res = { data?: { items: { name: string }[] } };
const responses: Res[] = [{ data: { items: [{ name: 'Mouse' }] } }, { data: { items: [] } }, {}];
for (const r of responses) console.log(r.data?.items[0]?.name ?? 'no items');

Output

Mouse
no items
no items

Explanation: ?. on both data and items[0] prevents errors when either is missing.

25. Merge config with spreadReal-world

Merge typed default and override configs, letting overrides win.

Show solution
type Config = { baseURL: string; retries: number; headless: boolean };
const defaults: Config = { baseURL: 'http://localhost:3000', retries: 0, headless: true };
const overrides: Partial<Config> = { retries: 2, baseURL: 'https://qa.app.test' };
const finalConfig: Config = { ...defaults, ...overrides };
console.log(finalConfig);

Output

{ baseURL: 'https://qa.app.test', retries: 2, headless: true }

Explanation: Partial&lt;Config&gt; makes every property optional, and spreading it over the defaults produces a complete Config.

26. Type guard for API statusReal-world

Narrow a status code to success or failure with a type guard.

Show solution
type SuccessCode = 200 | 201 | 204;
const isSuccess = (code: number): code is SuccessCode => code === 200 || code === 201 || code === 204;
for (const code of [200, 204, 404, 500]) {
  console.log(code, isSuccess(code) ? 'success' : code >= 500 ? 'server error' : 'client error');
}

Output

200 success
204 success
404 client error
500 server error

Explanation: a custom type guard returns a boolean at runtime and narrows the type at compile time.

27. Retry decision with grouped logicReal-world

Decide whether to retry based on typed failure details.

Show solution
type Failure = { status: number; attempt: number; idempotent: boolean };
const MAX = 3;
const shouldRetry = (f: Failure): boolean => f.attempt < MAX && (f.status >= 500 || f.status === 429) && f.idempotent;
const cases: Failure[] = [
  { status: 503, attempt: 1, idempotent: true },
  { status: 404, attempt: 1, idempotent: true },
  { status: 429, attempt: 3, idempotent: true },
  { status: 500, attempt: 1, idempotent: false }
];
for (const c of cases) console.log(c.status, c.attempt, shouldRetry(c));

Output

503 1 true
404 1 false
429 3 false
500 1 false

Explanation: parentheses group the status rule, and typed input makes the rule easy to test case by case.

28. Convert milliseconds with integer operatorsReal-world

Convert durations to m:ss using Math.floor, / and %.

Show solution
const toClock = (ms: number): string => {
  const total = Math.floor(ms / 1000);
  return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
};
for (const ms of [5000, 65000, 754000]) console.log(ms, '->', toClock(ms));

Output

5000 -> 0:05
65000 -> 1:05
754000 -> 12:34

Explanation: division gives whole minutes and % gives the remaining seconds.

29. Version comparisonReal-world

Compare semantic versions numerically.

Show solution
function compareVersions(a: string, b: string): -1 | 0 | 1 {
  const pa = a.split('.').map(Number);
  const pb = b.split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    if (pa[i] > pb[i]) return 1;
    if (pa[i] < pb[i]) return -1;
  }
  return 0;
}
console.log(compareVersions('1.48.2', '1.50.0'), compareVersions('2.0.0', '1.9.9'), compareVersions('1.2.3', '1.2.3'));

Output

-1 1 0

Explanation: the literal return type -1 | 0 | 1 documents exactly which values the function can produce.

30. Exhaustive switch on a unionReal-world

Map browser names to engines, with a compile-time check that all cases are handled.

Show solution
type BrowserName = 'chromium' | 'firefox' | 'webkit';
function engine(b: BrowserName): string {
  switch (b) {
    case 'chromium': return 'Blink';
    case 'firefox': return 'Gecko';
    case 'webkit': return 'WebKit';
    default: { const unhandled: never = b; return unhandled; }
  }
}
const all: BrowserName[] = ['chromium', 'firefox', 'webkit'];
console.log(all.map(engine).join(', '));

Output

Blink, Gecko, WebKit

Explanation: the never assignment fails to compile if a new browser is added to the union but not handled.

Keep Practising

Leave a Comment