Skip to content

Objects, Arrays & Tuples

An object type describes a shape by listing properties. Two modifiers carry a lot of meaning:

interface User {
readonly id: string; // can't be reassigned after creation
name: string; // required
email?: string; // optional — string | undefined
}
  • readonly blocks reassignment of a property. It’s compile-time only (nothing stops mutation at runtime), but it documents and enforces intent.
  • ? (optional) means the property may be absent. Crucially, an optional property has type T | undefined, so you must handle the undefined case:
function greet(u: User) {
u.email.toLowerCase();
// ❌ Object is possibly 'undefined'.
u.email?.toLowerCase(); // ✅ optional chaining handles the absence
}

interface and type are nearly interchangeable for object shapes; interface can be re-opened (declaration merging) and is idiomatic for public object contracts, while type can also express unions, tuples, and mapped types. Use type when you need those; either is fine for a plain object.

When keys aren’t known ahead of time, an index signature types them uniformly:

interface Scores {
[player: string]: number;
}
const s: Scores = { ada: 10, alan: 12 };
s.grace = 9; // ✅ any string key, number value

The cost is honesty about absence: s.someone is typed number but is actually undefined at runtime. With the noUncheckedIndexedAccess compiler option, TypeScript makes this real by typing every index access as number | undefined — a strongly recommended setting.

An array is a homogeneous, variable-length list: number[] is “any number of numbers.” A tuple is a fixed-length list where each position has its own type:

let list: number[] = [1, 2, 3, 4]; // any length, all number
let point: [number, number] = [10, 20]; // exactly two numbers
let pair: [string, number] = ["age", 36]; // position 0 string, position 1 number

Tuples power patterns like React’s useState return ([value, setter]) and named-ish coordinates. They also support labels (documentation only, no runtime effect) and rest elements:

type HttpResult = [status: number, body: string];
type Args = [first: string, ...rest: number[]];
flowchart TB
  arr["number[]
any length · all same type"] --> arrEx["[1, 2, 3, 4, ...]"]
  tup["[string, number]
fixed length · per-position type"] --> tupEx["['age', 36]"]
Array is homogeneous and variable; tuple is positional and fixed

readonly number[] (or ReadonlyArray<number>) forbids mutating methods like push and index assignment — great for parameters you promise not to change. And recall from the last lesson: as const on an array literal produces exactly a readonly tuple of literals:

const rgb = [255, 128, 0] as const;
// type: readonly [255, 128, 0]
function paint(color: readonly number[]) { /* can't mutate color */ }

Preferring readonly array parameters is a cheap, high-value habit: it prevents a whole class of accidental-mutation bugs and communicates ownership.

What is the type of an optional property `email?: string`?
What does the `noUncheckedIndexedAccess` option change?
What is the difference between `number[]` and `[number, number]`?
What type does `[255, 128, 0] as const` produce?