Practise TypeScript arrays and tuples 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 Arrays lesson first.
- Beginner programs (1–10)
- Intermediate programs (11–22)
- Real-world QA scenario programs (23–31)
TypeScript Beginner Programs
1. Declare typed arraysBeginner
Declare a string[] of browsers and a number[] of timeouts, then print their lengths.
Show solution
const browsers: string[] = ['chromium', 'firefox', 'webkit'];
const timeouts: number[] = [5000, 10000, 30000];
console.log(browsers.length, timeouts.length);
console.log(browsers[0], timeouts[2]);
Output
3 3
chromium 30000
Explanation: the element type is fixed by the annotation, so every item is checked when the array is created.
2. Wrong element types are compile errorsBeginner
Show that pushing a number into a string[] is rejected.
Show solution
const tags: string[] = ['@smoke'];
tags.push('@regression');
// @ts-expect-error: Argument of type 'number' is not assignable to parameter of type 'string'
tags.push(42);
console.log(tags.length);
Output
3
Explanation: the compiler stops the bad push. The line still runs here for the demo, which is why the length is 3; in a real build the error would stop you first.
3. Array<T> generic syntaxBeginner
Declare the same kind of array with Array<number> and sum it.
Show solution
const scores: Array<number> = [90, 75, 88];
let total = 0;
for (const s of scores) total += s;
console.log(`total ${total}, average ${(total / scores.length).toFixed(1)}`);
Output
total 253, average 84.3
Explanation: Array<number> and number[] mean exactly the same thing. Most code uses number[]; the generic form reads better for complex types.
4. Inferred array typeBeginner
Let TypeScript infer the type of an array literal and use a string method on an item.
Show solution
const pages = ['home', 'login', 'cart'];
const upper = pages.map(p => p.toUpperCase());
console.log(upper.join(' | '));
Output
HOME | LOGIN | CART
Explanation: the literal is inferred as string[], so p is a string inside map without any annotation.
5. Union element typeBeginner
Store test ids that may be numbers or strings and print each with its type.
Show solution
const ids: (string | number)[] = [101, 'TC-202', 303];
for (const id of ids) console.log(`${id} is ${typeof id}`);
Output
101 is number
TC-202 is string
303 is number
Explanation: the parentheses matter: (string | number)[] is an array of mixed items, while string | number[] would mean a string or an array of numbers.
6. Readonly arrayBeginner
Create a readonly string[] of environments and show that it cannot be changed.
Show solution
const envs: readonly string[] = ['dev', 'staging', 'prod'];
console.log(envs.includes('staging'));
// @ts-expect-error: Property 'push' does not exist on type 'readonly string[]'
const add = () => envs.push('qa');
console.log(envs.length, typeof add);
Output
true
3 function
Explanation: a readonly array has no mutating methods in its type, so push, pop and index assignment fail to compile.
7. Tuple for a key-value pairBeginner
Model a header as a [string, string] tuple and print it.
Show solution
const header: [string, string] = ['Content-Type', 'application/json'];
const [name, value] = header;
console.log(`${name}: ${value}`);
Output
Content-Type: application/json
Explanation: a tuple fixes both the length and the type at each position, and destructuring keeps those types.
8. Tuple with mixed typesBeginner
Store a test result as [string, boolean, number].
Show solution
const result: [string, boolean, number] = ['login test', true, 1240];
const [title, passed, ms] = result;
console.log(`${title}: ${passed ? 'PASS' : 'FAIL'} in ${ms}ms`);
Output
login test: PASS in 1240ms
Explanation: each position has its own type, so passed is a boolean and ms is a number after destructuring.
9. Filter with a typed callbackBeginner
Keep only the failed tests from a list of statuses.
Show solution
const statuses: string[] = ['passed', 'failed', 'passed', 'skipped', 'failed'];
const failed = statuses.filter(s => s === 'failed');
console.log(`${failed.length} failed of ${statuses.length}`);
Output
2 failed of 5
Explanation: filter returns the same element type, string[], so the result needs no annotation.
10. Map numbers to stringsBeginner
Turn durations in milliseconds into labelled seconds.
Show solution
const durations: number[] = [1200, 450, 3100];
const labels: string[] = durations.map(ms => `${(ms / 1000).toFixed(2)}s`);
console.log(labels.join(', '));
Output
1.20s, 0.45s, 3.10s
Explanation: map changes the element type from number to string, and the annotation on labels confirms it.
TypeScript Intermediate Programs
11. reduce with a typed accumulatorIntermediate
Total the run time of tests with reduce.
Show solution
const runs: number[] = [320, 1500, 780, 95];
const totalMs = runs.reduce((sum: number, ms: number) => sum + ms, 0);
console.log(`total ${totalMs}ms`);
Output
total 2695ms
Explanation: the starting value 0 gives the accumulator its number type, so the result is a number.
12. find returns T | undefinedIntermediate
Find a user by role and handle the case where none matches.
Show solution
const users: string[] = ['admin@test.io', 'viewer@test.io'];
const editor = users.find(u => u.startsWith('editor'));
console.log(editor ?? 'no editor found');
const admin = users.find(u => u.startsWith('admin'));
if (admin) console.log(admin.toUpperCase());
Output
no editor found
ADMIN@TEST.IO
Explanation: find returns string | undefined, so TypeScript makes you check the result before calling string methods on it.
13. Array of objectsIntermediate
Type a list of test cases and print the ones tagged as smoke tests.
Show solution
type TestCase = { id: number; title: string; tags: string[] };
const cases: TestCase[] = [
{ id: 1, title: 'login', tags: ['smoke'] },
{ id: 2, title: 'search', tags: ['regression'] },
{ id: 3, title: 'checkout', tags: ['smoke', 'payments'] },
];
const smoke = cases.filter(c => c.tags.includes('smoke')).map(c => c.title);
console.log(smoke.join(', '));
Output
login, checkout
Explanation: once the element type is named, every callback knows the shape of c, and misspelled properties fail to compile.
14. Sort numbers correctlyIntermediate
Sort response times in ascending order.
Show solution
const times: number[] = [250, 1200, 90, 400];
const sorted = [...times].sort((a, b) => a - b);
console.log(sorted.join(' < '));
console.log(times.join(', '));
Output
90 < 250 < 400 < 1200
250, 1200, 90, 400
Explanation: without a compare function sort compares as strings, so 1200 would come before 250. Copying with spread keeps the original untouched.
15. Sort objects by a propertyIntermediate
Sort test results from slowest to fastest.
Show solution
type Result = { name: string; ms: number };
const results: Result[] = [
{ name: 'login', ms: 820 },
{ name: 'search', ms: 2100 },
{ name: 'logout', ms: 300 },
];
results.sort((a, b) => b.ms - a.ms);
console.log(results.map(r => `${r.name}:${r.ms}`).join(' '));
Output
search:2100 login:820 logout:300
Explanation: the compare function is typed from Result, so a.ms and b.ms are numbers.
16. Remove duplicates with SetIntermediate
Remove duplicate tags while keeping a string[] type.
Show solution
const tags: string[] = ['smoke', 'ui', 'smoke', 'api', 'ui'];
const unique: string[] = [...new Set(tags)];
console.log(unique.join(', '));
Output
smoke, ui, api
Explanation: new Set(tags) is a Set<string>, and spreading it back gives a string[].
17. Type guard in filterIntermediate
Remove null values from a list and get a string[] back.
Show solution
const raw: (string | null)[] = ['a@test.io', null, 'b@test.io', null];
const emails = raw.filter((e): e is string => e !== null);
console.log(emails.map(e => e.toUpperCase()).join(', '));
Output
A@TEST.IO, B@TEST.IO
Explanation: the e is string predicate tells TypeScript the result is string[]. Without it the type would stay (string | null)[].
18. Optional tuple elementIntermediate
Model a viewport as [width, height, scale?].
Show solution
type Viewport = [number, number, number?];
const desktop: Viewport = [1280, 720];
const retina: Viewport = [1440, 900, 2];
for (const [w, h, scale = 1] of [desktop, retina]) console.log(`${w}x${h} @${scale}x`);
Output
1280x720 @1x
1440x900 @2x
Explanation: the ? makes the last position optional, and a default in the destructuring fills it in.
19. Named tuple elementsIntermediate
Use labelled tuple members to describe a retry setting.
Show solution
type RetryPolicy = [attempts: number, delayMs: number];
const policy: RetryPolicy = [3, 500];
const [attempts, delayMs] = policy;
console.log(`retry ${attempts} times every ${delayMs}ms`);
Output
retry 3 times every 500ms
Explanation: labels do not change the runtime value, but they show up in editor hints, which makes tuples self-documenting.
20. Readonly tuple with as constIntermediate
Freeze a list of browsers with as const and use it as a union type.
Show solution
const BROWSERS = ['chromium', 'firefox', 'webkit'] as const;
type Browser = (typeof BROWSERS)[number];
function launch(b: Browser): string {
return `launching ${b}`;
}
console.log(launch('firefox'));
console.log(BROWSERS.length);
Output
launching firefox
3
Explanation: as const makes a readonly tuple of literal types, and [number] turns it into the union 'chromium' | 'firefox' | 'webkit'.
21. Rest elements in a tupleIntermediate
Type a log line as a level followed by any number of messages.
Show solution
type LogLine = [level: 'info' | 'error', ...messages: string[]];
const lines: LogLine[] = [
['info', 'page loaded'],
['error', 'timeout', 'retrying'],
];
for (const [level, ...msgs] of lines) console.log(`[${level}] ${msgs.join(' / ')}`);
Output
[info] page loaded
[error] timeout / retrying
Explanation: a rest element lets a tuple have a fixed start and a variable tail, still fully typed.
22. Two-dimensional arrayIntermediate
Store a small test data grid as string[][] and print it.
Show solution
const grid: string[][] = [
['user', 'role'],
['ana', 'admin'],
['ben', 'viewer'],
];
for (const row of grid) console.log(row.join(' | '));
console.log(`rows ${grid.length}, cols ${grid[0].length}`);
Output
user | role
ana | admin
ben | viewer
rows 3, cols 2
Explanation: string[][] is an array whose items are string[], so each row has array methods.
TypeScript Real-World QA Scenario Programs
23. Group items into a recordReal-world
Group test names by status into Record<string, string[]>.
Show solution
type Run = { name: string; status: 'passed' | 'failed' | 'skipped' };
const runs: Run[] = [
{ name: 'login', status: 'passed' },
{ name: 'cart', status: 'failed' },
{ name: 'search', status: 'passed' },
{ name: 'profile', status: 'skipped' },
];
const byStatus: Record<string, string[]> = {};
for (const r of runs) (byStatus[r.status] ??= []).push(r.name);
for (const [status, names] of Object.entries(byStatus)) console.log(`${status}: ${names.join(', ')}`);
Output
passed: login, search
failed: cart
skipped: profile
Explanation: ??= creates the list the first time a status appears, and the record type keeps every value a string[].
24. Chunk an array genericallyReal-world
Split a list of test files into batches of a given size.
Show solution
function chunk<T>(items: T[], size: number): T[][] {
const out: T[][] = [];
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
return out;
}
const files = ['a.spec', 'b.spec', 'c.spec', 'd.spec', 'e.spec'];
chunk(files, 2).forEach((batch, i) => console.log(`worker ${i + 1}: ${batch.join(', ')}`));
Output
worker 1: a.spec, b.spec
worker 2: c.spec, d.spec
worker 3: e.spec
Explanation: the generic T means the same function chunks strings, numbers or objects, and the result keeps the element type.
25. Generic last-item helperReal-world
Write last<T>(items: T[]): T | undefined.
Show solution
function last<T>(items: readonly T[]): T | undefined {
return items[items.length - 1];
}
console.log(last(['step 1', 'step 2', 'step 3']));
console.log(last([10, 20]));
console.log(last([]) ?? 'empty');
Output
step 3
20
empty
Explanation: the return type includes undefined because the array may be empty. Taking readonly T[] lets callers pass frozen arrays too.
26. Zip two arrays into tuplesReal-world
Pair test names with their durations.
Show solution
function zip<A, B>(a: A[], b: B[]): [A, B][] {
const n = Math.min(a.length, b.length);
const out: [A, B][] = [];
for (let i = 0; i < n; i++) out.push([a[i], b[i]]);
return out;
}
const pairs = zip(['login', 'search', 'cart'], [820, 2100, 640]);
for (const [name, ms] of pairs) console.log(`${name.padEnd(7)}${ms}ms`);
Output
login 820ms
search 2100ms
cart 640ms
Explanation: two type parameters keep each side’s type, so name is a string and ms is a number in the loop.
27. Partition into pass and failReal-world
Split results into two arrays in one pass, returning a tuple.
Show solution
function partition<T>(items: T[], test: (item: T) => boolean): [T[], T[]] {
const yes: T[] = [];
const no: T[] = [];
for (const item of items) (test(item) ? yes : no).push(item);
return [yes, no];
}
const codes = [200, 404, 201, 500, 204];
const [ok, bad] = partition(codes, c => c < 400);
console.log(`ok: ${ok.join(',')} | bad: ${bad.join(',')}`);
Output
ok: 200,201,204 | bad: 404,500
Explanation: returning a tuple [T[], T[]] lets the caller destructure both groups with the right types.
28. Count by key with a MapReal-world
Count how many times each error message appears.
Show solution
const errors: string[] = ['timeout', 'not found', 'timeout', 'timeout', 'not found', 'crash'];
const counts = new Map<string, number>();
for (const e of errors) counts.set(e, (counts.get(e) ?? 0) + 1);
const top = [...counts].sort((a, b) => b[1] - a[1]);
for (const [msg, n] of top) console.log(`${n} x ${msg}`);
Output
3 x timeout
2 x not found
1 x crash
Explanation: Map<string, number> keeps keys and counts typed, and spreading a map gives [string, number][] for sorting.
29. Flatten nested arraysReal-world
Flatten a list of test suites into one list of tests.
Show solution
const suites: string[][] = [['login', 'logout'], ['search'], ['cart', 'checkout']];
const tests: string[] = suites.flat();
console.log(`${tests.length} tests: ${tests.join(', ')}`);
const steps = suites.flatMap(s => s.map(t => `run ${t}`));
console.log(steps[0], '...', steps[steps.length - 1]);
Output
5 tests: login, logout, search, cart, checkout
run login ... run checkout
Explanation: flat() on string[][] returns string[], and flatMap maps and flattens one level in a single call.
30. Pass rate per suiteReal-world
Compute the pass rate for each suite from a typed array of results.
Show solution
type Outcome = { suite: string; passed: boolean };
const outcomes: Outcome[] = [
{ suite: 'auth', passed: true },
{ suite: 'auth', passed: false },
{ suite: 'cart', passed: true },
{ suite: 'cart', passed: true },
];
const stats = new Map<string, [pass: number, total: number]>();
for (const o of outcomes) {
const [pass, total] = stats.get(o.suite) ?? [0, 0];
stats.set(o.suite, [pass + (o.passed ? 1 : 0), total + 1]);
}
for (const [suite, [pass, total]] of stats) console.log(`${suite}: ${Math.round((pass / total) * 100)}%`);
Output
auth: 50%
cart: 100%
Explanation: storing a named tuple in the map keeps two related numbers together with clear labels.
31. Readonly parameter protects the callerReal-world
Write a function that returns a sorted copy without changing a readonly input.
Show solution
function sortedCopy(values: readonly number[]): number[] {
// @ts-expect-error: Property 'sort' does not exist on type 'readonly number[]'
const misuse = () => values.sort();
return [...values].sort((a, b) => a - b);
}
const original = [3, 1, 2] as const;
console.log(sortedCopy(original).join(','), original.join(','));
Output
1,2,3 3,1,2
Explanation: sort changes the array in place, so it is not allowed on a readonly type. Copy first, then sort; the caller’s data stays the same.