Right, let’s talk about arrays. You’ve got a bunch of things—numbers, strings, whatever—and you want to keep them in a nice, orderly line. TypeScript, being the helpful but occasionally pedantic friend that it is, demands to know what kind of things you’re putting in that line. This is where array types come in, and you’ve got two syntaxes to choose from. They do the same thing. Mostly. We’ll get to that.

The two flavors are T[] and Array<T>. Read the first one as “an array of T” and the second one as “Array of T”. T is your type parameter—string, number, boolean, your own custom interface, you name it.

// The bracket notation: clean and very common.
const luckyNumbers: number[] = [1, 2, 3, 7, 14];

// The generic notation: feels more explicit to some.
const petNames: Array<string> = ['Rover', 'Spot', 'Ms. Fluffernutter'];

You can use them totally interchangeably. It’s like choosing between a flat-head or a Phillips-head screwdriver for a screw that accepts both. The end result is identical. I prefer T[] for simple types because it’s less noisy, but I’ll reach for Array<T> when the type itself is complex and needs its own generics, just for clarity. Compare:

// This starts to look like a regex for a sadist.
const complexArray: Array<Array<{ id: number; name: string }>> = [[{ id: 1, name: 'foo' }]];

// Slightly easier on the eyes? Maybe?
const complexArray2: { id: number; name: string }[][] = [[{ id: 1, name: 'foo' }]];

ReadonlyArray: The Immutable Hammer

Here’s where the syntax choice actually matters. You can’t just slap readonly in front of Array<T>. Nope. For immutability, you have to use the specific ReadonlyArray<T> type (or readonly T[]).

const mutableArray: number[] = [1, 2, 3];
mutableArray.push(4); // All good.
mutableArray[0] = 999; // No problem.

const readOnlyVersion: ReadonlyArray<number> = [1, 2, 3];
// readOnlyVersion.push(4); // Error: Property 'push' does not exist on type 'ReadonlyArray<number>'.
// readOnlyVersion[0] = 999; // Error: Index signature in type 'readonly number[]' only permits reading.

// The newer, sleeker syntax:
const alsoReadOnly: readonly number[] = [1, 2, 3];

Use this everywhere you don’t intend to modify the array. It’s a fantastic way to tell other developers (and your future forgetful self) that this function won’t mess with your input. It’s a promise, enforced by the compiler.

The “Any” Escape Hatch (And Why You Should Avoid It)

You can, of course, declare an array of any. any[] is the equivalent of turning off the fire alarm because you’re just making some toast. It might be fine, but the potential for disaster is spectacular.

const mysteryBag: any[] = [1, 'a string', { a: true }, Promise.resolve()];
mysteryBag.forEach(item => {
  console.log(item.whatEvenIsThis); // No errors. Happy coding!
  // Runtime error? Probably.
});

If you find yourself using any[], you’ve almost certainly lost. Reach for unknown[] instead. It forces you to do proper type checking before you do anything dangerous with the elements.

Pitfalls: Empty Arrays and Type Widening

Here’s a classic “gotcha” that bites everyone eventually. You declare an empty array, TypeScript sees no elements to infer a type from, and it does the “safe” thing: it infers never[]. Then you try to push a number into it and get a confusing error.

const initiallyEmpty = []; // TypeScript infers type: never[]
// initiallyEmpty.push(1); // Error: Argument of type 'number' is not assignable to parameter of type 'never'.

// The fix? Annotate, annotate, annotate.
const properlyTypedEmpty: number[] = [];
properlyTypedEmpty.push(1); // Works perfectly.

Another subtle one is type widening within inferences. This is less common with primitives but can be a headache with more complex types and as const.

Tuples: Arrays with a Personality Disorder

While we’re here, I have to mention tuples, because they look like arrays but are a different beast entirely. An array is a list of things of the same type. A tuple is a fixed-length list where each position can have a different, specific type. They are the over-engineered, meticulously labeled organizer for your data.

// A regular array of strings or numbers? Nope.
const simpleArray: (string | number)[] = ['hello', 42, 'world']; // Length can change.

// A tuple: fixed length, specific order.
const myTuple: [string, number, boolean] = ['hello', 42, true];
// myTuple[3] = 'extra'; // Error: Type '"extra"' is not assignable to type 'undefined'.

// A classic use case: React's useState returns a tuple.
const [state, setState] = useState(initialState); // [T, Dispatch<SetStateAction<T>>]

The key difference? Arrays are about sequences of a type; tuples are about structured data. If you find yourself writing [string, number], you’re probably defining a tuple. If you’re writing string[], you’ve got a list. Don’t mix them up, or TypeScript will give you a very stern talking-to.