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.
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.
- Beginner programs (1–10)
- Intermediate programs (11–20)
- Real-world QA scenario programs (21–30)
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 &.
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: && 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<Config> 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.