Test code is full of collections — a list of supported browsers, an array of test users, the rows returned by a table query. In JavaScript for QA you used arrays freely without saying what they held. TypeScript asks you to be specific: an array of what? This lesson covers array types, tuples (their fixed-shape cousins), and readonly for collections that should never change.
Two array syntaxes — same thing
There are two ways to write an array type in TypeScript. They mean exactly the same thing.
const browsers: string[] = ["Chrome", "Firefox", "Safari"];
const browsers2: Array<string> = ["Chrome", "Firefox", "Safari"];string[] is shorter and the more common style — most codebases prefer it. Array<string> exists for consistency with other generic types (Promise<T>, Map<K, V>) and is occasionally clearer when nested. Pick string[] as your default.
Arrays are typed — strictly
A number[] only accepts numbers. A string[] only accepts strings. The compiler enforces this on every operation.
const codes: number[] = [200, 201, 404];
codes.push(500); // ✅ adds a number
codes.push("500");
// ❌ Argument of type 'string' is not assignable to parameter of type 'number'.In JavaScript you could push anything into anything, and the bug would surface much later — usually during an assertion that compared a number to a string-shaped-like-a-number. TypeScript blocks the mistake at the line you wrote it.
Arrays of objects
Real test data is rarely a flat list of strings. It's a list of users, a list of test cases, a list of API responses. Inline object shapes work fine for this:
const users: { name: string; role: string }[] = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "tester" },
];
users.push({ name: "Carol", role: "viewer" }); // ✅
users.push({ name: "Dave" });
// ❌ Property 'role' is missing in type '{ name: string; }'.That { name: string; role: string }[] annotation gets unwieldy fast. The next chapter introduces interface and type — names you give to object shapes so you can write User[] instead.
Tuples — fixed length, specific types per position
A tuple is an array with a fixed number of elements, where each position has a specific type. Useful when the shape is "two strings followed by a number" rather than "a list of any length."
const testResult: [string, boolean, number] = ["Login test", true, 1250];
// testResult[0] is string (test name)
// testResult[1] is boolean (passed)
// testResult[2] is number (duration ms)
testResult[0] = "Logout test"; // ✅
testResult[1] = "yes";
// ❌ Type 'string' is not assignable to type 'boolean'.
const broken: [string, boolean, number] = ["Login", true];
// ❌ Source has 2 element(s) but target requires 3.The length is part of the contract. So is the type at every position. This is more rigid than an array and that's exactly the point — tuples make the structure of the data part of its type.
Named tuples — labels for clarity
Plain tuples have one weakness: positional access is cryptic. What's testResult[2] again? TypeScript 4.0 added named tuple labels that document each slot:
type TestResult = [name: string, passed: boolean, duration: number];
const result: TestResult = ["Login test", true, 1250];
// Hovering over result in VS Code now shows:
// const result: [name: string, passed: boolean, duration: number]The labels are documentation — they don't change runtime behaviour, but they show up in IDE tooltips and error messages, making tuples bearable to read. That said, if you have more than three or four fields, an object (with interface or type, next chapter) is almost always the better fit.
readonly arrays — collections that shouldn't change
Some collections should never be mutated after they're declared: a list of valid status codes, supported browsers, environment names. Mark them readonly and TypeScript blocks every mutating method.
const validStatuses: readonly number[] = [200, 201, 204];
validStatuses.push(500);
// ❌ Property 'push' does not exist on type 'readonly number[]'.
validStatuses[0] = 999;
// ❌ Index signature in type 'readonly number[]' only permits reading.
// Reads still work:
console.log(validStatuses.includes(200)); // ✅ truereadonly is your tool for constants that look like arrays but should behave like immutable values. Pair it with as const for compile-time-frozen literal data:
const browsers = ["chromium", "firefox", "webkit"] as const;
// type: readonly ["chromium", "firefox", "webkit"]Arrays vs tuples — when to use which
Arrays vs tuples — same brackets, different contracts
Arrays — string[]
Variable length — push, pop, splice freely
Every element has the same type
Indexed by position but each slot is interchangeable
Right for: a list of users, status codes, browsers
If the count varies, you want an array
Tuples — [string, boolean, number]
Fixed length — count is part of the type
Each position has its own type
Position is meaningful — [0] vs [1] vs [2]
Right for: a single test row, a (status, body) pair
If the shape is fixed and small, a tuple expresses it
In practice, arrays of objects beat tuples for most QA data. Reach for tuples when the shape is genuinely small and positional — like the [error, value] return pattern, or [width, height] coordinates.
A QA-flavoured example
A function that takes a list of test names plus a list of pass/fail flags and returns the count of passed tests:
function countPassed(names: string[], passed: boolean[]): number {
let count = 0;
for (let i = 0; i < names.length; i++) {
if (passed[i]) count++;
}
return count;
}
const names: string[] = ["Login", "Logout", "Search"];
const passed: boolean[] = [true, false, true];
console.log(countPassed(names, passed)); // 2The signature (names: string[], passed: boolean[]) => number documents every input and the output. A caller passing countPassed(passed, names) (arguments swapped) would be flagged by the compiler — boolean[] doesn't match string[].
⚠️ Common mistakes
- Mixing types in an array without a union.
const codes = [200, 201, "500"];— TypeScript infers(number | string)[]and now every consumer has to handle both. If the array is meant to be only numbers, use: number[]and fix the source of the string. If both types are legitimate, use: (number | string)[]and narrow when reading. You'll meet union types properly in the next lesson. - Reaching for tuples when an object is clearer.
[string, boolean, number]saves a few characters but makes every read site cryptic. Prefer an object with named fields once you go beyond two elements — your future self will thank you. - Forgetting
readonlyon shared constants. A team-wideBROWSERSlist that gets accidentallypush-ed inside a test helper can produce maddening cross-test contamination.readonlyis the cheapest way to prevent that — one keyword, total guarantee.
🎯 Practice task
Type a small test runner. 20-30 minutes.
- In your
ts-for-qa/srcfolder, createrunner.ts. - Declare these collections:
browsers: readonly string[]—["chromium", "firefox", "webkit"]validStatuses: readonly number[]—[200, 201, 204, 304]testCases: { name: string; passed: boolean; duration: number }[]— three sample rows
- Write a function
summarise(cases: { name: string; passed: boolean; duration: number }[]): { passed: number; failed: number; totalMs: number }that loops over the cases and tallies the result. - Call
summarise(testCases)andconsole.logthe result. Run withnpx ts-node src/runner.ts. - Try to mutate a readonly array —
browsers.push("edge"). Read the compile error. - Use a tuple — declare
type Coord = [x: number, y: number]; const point: Coord = [10, 20];and try assigning a third element. Read the error. - Stretch: rewrite the testCases array using a named tuple
type Row = [name: string, passed: boolean, duration: number]and rewritesummariseto acceptRow[]. Compare which version reads more clearly. (Hint: you'll probably prefer the object version — that's the lesson.)
The next lesson introduces union and literal types — the toolkit for "this value can be one of these specific things, and nothing else."