Arrays and Tuples

8 min read

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));  // ✅ true

readonly 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));   // 2

The 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 readonly on shared constants. A team-wide BROWSERS list that gets accidentally push-ed inside a test helper can produce maddening cross-test contamination. readonly is the cheapest way to prevent that — one keyword, total guarantee.

🎯 Practice task

Type a small test runner. 20-30 minutes.

  1. In your ts-for-qa/src folder, create runner.ts.
  2. 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
  3. 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.
  4. Call summarise(testCases) and console.log the result. Run with npx ts-node src/runner.ts.
  5. Try to mutate a readonly arraybrowsers.push("edge"). Read the compile error.
  6. Use a tuple — declare type Coord = [x: number, y: number]; const point: Coord = [10, 20]; and try assigning a third element. Read the error.
  7. Stretch: rewrite the testCases array using a named tuple type Row = [name: string, passed: boolean, duration: number] and rewrite summarise to accept Row[]. 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."

// tip to track lessons you complete and pick up where you left off across devices.