Chapter 4 of 24 · TypeScript for Beginners — Complete Free Tutorial
Arrays, Tuples and Objects
Single values were the warm-up; real programs in the TypeScript for Beginners series move in groups. This chapter types the three containers you will use constantly — arrays for lists of one kind of thing, tuples for fixed recipes of different things, and object types for the shape of your data — along with optional properties, index signatures, readonly guards, and what “shape typing” actually means.
Arrays of one type
An array of strings is written string[], or equivalently Array<string> — the two forms are interchangeable, and beginners often prefer the bracket form for simple types and the angle-bracket form for nested ones:
const titles: string[] = ["Basics", "Types", "Interfaces"];
const scores: number[] = [91, 84, 77];
const names: Array<string> = ["Ada", "Grace"];
scores.push("high");
Expected output:
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
The guarantee is two-sided: nothing wrong can go in, and — just as valuable — every element read out is known to be a number, so scores[0].toFixed() needs no guard. Lists genuinely holding a mixture declare it honestly with a union such as (string | number)[], which you will meet properly in chapter 7. Remember from the JavaScript series that arrays are objects: .length, .push() and .map() all type-check against the element type.
Tuples: fixed recipes
Sometimes the positions matter as much as the values: a coordinate pair, a [key, value] row, a two-item response. A tuple states the type at every index:
let entry: [string, number] = ["chapters", 24];
entry = [42, "chapters"];
const third = entry[2];
Expected output:
error TS2322: Type '[number, string]' is not assignable to type '[string, number]'.
error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
Order, length and per-index type are all enforced, and each index reads back as its declared type — entry[0] is a string, entry[1] is a number. Two refinements cover most real uses:
- Optional elements — a trailing
?allows the slot to be skipped, and its type widens to includeundefined: in[id: number, label?: string]the second element isstring | undefined. readonlytuples —const origin: readonly [number, number] = [0, 0]refuses writes: attemptingorigin.push(1)reportserror TS2339: Property 'push' does not exist on type 'readonly [number, number]'.
Named tuple elements such as [name: string, age: number] document each position while behaving exactly like the unnamed form. Use tuples for fixed-size data only — if it grows, it is an array.
Object literal types
An object type describes a shape: which properties exist, and what type each one holds. Inline, it looks like this:
const user: { name: string; age: number; active: boolean } = {
name: "Ada",
age: 36,
active: true,
};
From that point the compiler knows every property. Omit one and it says so precisely:
const broken: { name: string; age: number } = { name: "Ada" };
Expected output:
error TS2739: Type '{ name: string; }' is missing properties from type '{ name: string; age: number; }': 'age'.
And when a literal carries a property the shape never declared — the classic typo — the object is rejected rather than silently accepted:
const ada: { name: string; age: number } = {
name: "Ada",
age: 36,
emial: "ada@example.com",
};
Expected output:
error TS2353: Object literal may only specify known properties, and 'emial' does not exist in type '{ name: string; age: number; }'.
That check applies only to literals written on the spot; values arriving from variables are judged purely on shape. The distinction matters, and the next chapter explains it properly.
Optional properties
Real data has gaps, and a property that may be missing gets a ?:
const account: { id: number; nickname?: string } = { id: 7 };
console.log(account.nickname);
Expected output: undefined — but that is the honest result: the property is typed string | undefined, so the compiler requires a check before it is used. Optional means “may be absent”, never “may be the wrong type”: supplying nickname: 42 is still TS2322. Compare nickname?: string with nickname: string | undefined — the first also allows you to delete the property outright, which the second does not.
Index signatures: typed dictionaries
When property names are not known in advance — scores by subject, translations by locale — an index signature types every key at once:
const scores: { [subject: string]: number } = { maths: 90, english: 84 };
scores.history = 77;
scores.geography = "A";
Expected output:
error TS2322: Type 'string' is not assignable to type 'number'.
Every property must satisfy the signature, so the dictionary stays homogeneous no matter how it grows. Two consequences worth knowing:
- Any key looks valid —
scores.mathmtype-checks and returnsnumber, yet isundefinedat runtime. An index signature trades typo detection for flexibility. - Mixing types means a union — a report card with numbers and strings needs
{ [key: string]: number | string }, not two signatures.
readonly arrays, and what “shape” means
A readonly modifier protects the container itself. const stops you reassigning the variable; readonly stops you mutating what it points at:
const locked: readonly string[] = ["first"];
locked.push("second");
Expected output:
error TS2339: Property 'push' does not exist on type 'readonly string[]'.
The same idea applies to properties (readonly id: number) and to whole objects built with as const, which freezes literals deeply. Underneath all of this sits one principle you will rely on for the rest of the series: TypeScript checks the shape of a value, not its identity or its class. An object with the right properties in the right types satisfies the type — there is no nameplate, no constructor check, no instanceof. Inline shapes work fine for one or two uses, but copy-pasted shapes drift apart the moment a field changes, which is exactly the problem the next chapter solves by giving a shape a name.
Common mistakes
- Mixing up the two array forms —
string[]andArray<string>are identical; what never works isstring[]<number>or forgetting the brackets on a general type. - Treating optional like “wrong type allowed” —
?only permits absence; the value still has to match when it is present. - Using a tuple for a growing list — fixed length is the point of a tuple; a list that gains items belongs in an array.
- Trusting an index signature to catch typos — any key type-checks, so a misspelled subject compiles and fails quietly at runtime.
- Confusing
constwithreadonly—const list = [1]still allowslist.push(2); onlyreadonlyblocks mutation.
Practice checklist
- Build a
number[], push a string into it, and read the TS2345 message until it makes sense. - Create a
[string, number]tuple, pass the elements in the wrong order, then add an optional third element and observe its type. - Type an object literal with two properties, mistype one value, and add a property that does not exist — capture both error messages.
- Give an object an optional property, access it without a guard, and fix the complaint.
- Convert a plain array to
readonly, then trypushand an index assignment to see which one the compiler refuses.
What's next?
Copy-pasted shapes are asking for trouble — next you give them names with Interfaces and Type Aliases.
Frequently Asked Questions
TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.
You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.
No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.
If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.
Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.
strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.
any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.
Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.
Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.
Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.