TL;DR
Learn typed arrays, tuples, and objects in TypeScript. Build type-safe data structures with interfaces, readonly, and generics.
Key concepts
- TypeScript arrays
- TypeScript tuples
- typed data structures
- TypeScript objects
Data Structures in TypeScript
In this lesson, we'll explore TypeScript's data structures, including arrays, objects, tuples, and more. These structures help you organize and manage data effectively in your programs.
Arrays
Arrays store ordered collections of values of the same type:
Basic Arrays
// Array of numbers
const numbers: number[] = [1, 2, 3, 4, 5];
console.log(`Numbers: ${numbers}`);
// Alternative syntax
const fruits: Array<string> = ["apple", "banana", "orange"];
console.log(`Fruits: ${fruits}`);
// Accessing elements
console.log(`First fruit: ${fruits[0]}`);
console.log(`Last fruit: ${fruits[fruits.length - 1]}`);
Array Methods
const numbers: number[] = [1, 2, 3, 4, 5];
// Push: Add to end
numbers.push(6);
console.log(`After push: ${numbers}`);
// Pop: Remove from end
const last = numbers.pop();
console.log(`Popped: ${last}, Array: ${numbers}`);
// Shift: Remove from beginning
const first = numbers.shift();
console.log(`Shifted: ${first}, Array: ${numbers}`);
// Unshift: Add to beginning
numbers.unshift(0);
console.log(`After unshift: ${numbers}`);
Array Iteration
const fruits: string[] = ["apple", "banana", "orange"];
// forEach
console.log("Using forEach:");
fruits.forEach((fruit, index) => {
console.log(`${index}: ${fruit}`);
});
// map
const upperFruits = fruits.map(fruit => fruit.toUpperCase());
console.log(`Uppercase: ${upperFruits}`);
// filter
const longFruits = fruits.filter(fruit => fruit.length > 5);
console.log(`Long fruits: ${longFruits}`);
// reduce
const numbers: number[] = [1, 2, 3, 4, 5];
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(`Sum: ${sum}`);
Objects
Objects store key-value pairs and can represent complex data:
Basic Objects
// Object with type annotation
const person: { name: string; age: number; city: string } = {
name: "Alice",
age: 30,
city: "New York"
};
console.log(`Name: ${person.name}`);
console.log(`Age: ${person.age}`);
console.log(`City: ${person.city}`);
Type Aliases
// Define a reusable type
type Person = {
name: string;
age: number;
email?: string; // Optional property
};
const alice: Person = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
const bob: Person = {
name: "Bob",
age: 25
// email is optional
};
console.log(`Alice: ${alice.name}, ${alice.email}`);
console.log(`Bob: ${bob.name}`);
Interfaces
// Interface for object shape
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
const laptop: Product = {
id: 1,
name: "Laptop",
price: 999.99,
inStock: true
};
console.log(`Product: ${laptop.name}, Price: $${laptop.price}`);
Nested Objects
interface Address {
street: string;
city: string;
zipCode: string;
}
interface User {
name: string;
address: Address;
}
const user: User = {
name: "Alice",
address: {
street: "123 Main St",
city: "New York",
zipCode: "10001"
}
};
console.log(`${user.name} lives at ${user.address.street}, ${user.address.city}`);
Tuples
Tuples are fixed-length arrays with specific types for each position:
// Tuple: [string, number]
const person: [string, number] = ["Alice", 30];
console.log(`Name: ${person[0]}, Age: ${person[1]}`);
// Tuple with multiple types
const employee: [number, string, boolean] = [1, "Bob", true];
console.log(`ID: ${employee[0]}, Name: ${employee[1]}, Active: ${employee[2]}`);
// Destructuring tuples
const [personName, personAge] = person;
console.log(`Destructured - Name: ${personName}, Age: ${personAge}`);
Predict
A tuple like [number, number] is a fixed-length, position-typed array — but only when you say so. With NO annotation, what type does the compiler infer for point here?
const point = [10, 20];Enums
Enums define a set of named constants:
// Numeric enum
enum Direction {
North,
South,
East,
West
}
const currentDirection: Direction = Direction.North;
console.log(`Direction: ${currentDirection}`); // Prints: 0
// String enum
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
const favoriteColor: Color = Color.Blue;
console.log(`Favorite color: ${favoriteColor}`); // Prints: BLUE
Maps
Maps store key-value pairs with any type as keys:
// Create a Map
const scores = new Map<string, number>();
// Add entries
scores.set("Alice", 95);
scores.set("Bob", 87);
scores.set("Charlie", 92);
// Get values
console.log(`Alice's score: ${scores.get("Alice")}`);
// Check if key exists
console.log(`Has Bob? ${scores.has("Bob")}`);
// Iterate over entries
console.log("All scores:");
scores.forEach((score, name) => {
console.log(`${name}: ${score}`);
});
Sets
Sets store unique values:
// Create a Set
const uniqueNumbers = new Set<number>();
// Add values
uniqueNumbers.add(1);
uniqueNumbers.add(2);
uniqueNumbers.add(3);
uniqueNumbers.add(2); // Duplicate, won't be added
console.log(`Set size: ${uniqueNumbers.size}`);
// Check if value exists
console.log(`Has 2? ${uniqueNumbers.has(2)}`);
// Iterate over values
console.log("Values:");
uniqueNumbers.forEach(num => {
console.log(num);
});
Try It Yourself
Practice working with data structures in the playground below:
// Arrays
const numbers: number[] = [5, 10, 15, 20, 25];
console.log("Original array:", numbers);
// Array methods
const doubled = numbers.map(n => n * 2);
console.log("Doubled:", doubled);
const evenNumbers = numbers.filter(n => n % 2 === 0);
console.log("Even numbers:", evenNumbers);
const sum = numbers.reduce((total, n) => total + n, 0);
console.log("Sum:", sum);
// Objects
interface Book {
title: string;
author: string;
year: number;
pages: number;
}
const book: Book = {
title: "TypeScript Handbook",
author: "TypeScript Team",
year: 2024,
pages: 300
};
console.log(`\nBook: ${book.title} by ${book.author}`);
console.log(`Published: ${book.year}, Pages: ${book.pages}`);
// Tuples
const coordinate: [number, number] = [10, 20];
console.log(`\nCoordinate: (${coordinate[0]}, ${coordinate[1]})`);
// Array of objects
const students: { name: string; grade: number }[] = [
{ name: "Alice", grade: 95 },
{ name: "Bob", grade: 87 },
{ name: "Charlie", grade: 92 }
];
console.log("\nStudents:");
students.forEach(student => {
console.log(`${student.name}: ${student.grade}`);
});
// Find highest grade
const highest = students.reduce((max, student) =>
student.grade > max.grade ? student : max
);
console.log(`Highest grade: ${highest.name} with ${highest.grade}`);
// Map
const phoneBook = new Map<string, string>();
phoneBook.set("Alice", "555-1234");
phoneBook.set("Bob", "555-5678");
console.log("\nPhone book:");
phoneBook.forEach((number, name) => {
console.log(`${name}: ${number}`);
});
// Set
const tags = new Set<string>();
tags.add("typescript");
tags.add("javascript");
tags.add("programming");
tags.add("typescript"); // Duplicate, won't be added
console.log(`\nUnique tags (${tags.size}):`);
tags.forEach(tag => {
console.log(`- ${tag}`);
});
Try these exercises:
- Create an array of numbers and find the average
- Create an interface for a Car with properties like make, model, and year
- Use map() to convert an array of Celsius temperatures to Fahrenheit
- Create a tuple representing RGB color values
- Use a Map to store and retrieve shopping cart items
- Create a Set to remove duplicates from an array
Recall
Without scrolling up: array methods like map and filter take a function as their argument — for example fruits.map(fruit => fruit.toUpperCase()). What is that fruit => fruit.toUpperCase() piece, in the terms 03-functions used?
Reading about arrays and objects is not the same as building with them. This is a build task: a small program that reports its own pass/fail. You are given a learning log — an array of typed activity records — and three empty, fully typed functions to finish. Run it and it fails immediately, telling you which function is still missing. Implement each one until every check passes and it prints All checks passed.
The three functions reuse exactly what this lesson taught: looping over an array of objects typed by an interface, reading a field with dot notation, aggregating with a running total, and building up a Record as you go. The starter already has the data, the stubs, and the checks — you write only the body of each function. Nothing above spells out all three answers, so you will have to assemble them yourself.
Build
Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide what that function is missing, then implement the three functions until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.
import assert from "node:assert";
// The data: a learning log — an array of typed activity records. Do NOT change this array.
interface Activity {
title: string;
kind: string;
minutes: number;
}
const activities: Activity[] = [
{ title: "Intro to types", kind: "lesson", minutes: 25 },
{ title: "Union quiz", kind: "quiz", minutes: 10 },
{ title: "Narrowing drills", kind: "practice", minutes: 20 },
{ title: "Interfaces deep dive", kind: "lesson", minutes: 30 },
{ title: "Generics quiz", kind: "quiz", minutes: 15 },
];
// TODO 1: return a NEW array of just the titles, in order.
// titles(activities) -> ["Intro to types", "Union quiz", "Narrowing drills", "Interfaces deep dive", "Generics quiz"]
function titles(activities: Activity[]): string[] | undefined {
return undefined; // replace undefined with your code
}
// TODO 2: return the total minutes across all activities as a number.
// totalMinutes(activities) -> 100
function totalMinutes(activities: Activity[]): number | undefined {
return undefined; // replace undefined with your code
}
// TODO 3: return an object counting how many activities of each kind there are.
// countByKind(activities) -> { lesson: 2, quiz: 2, practice: 1 }
function countByKind(activities: Activity[]): Record<string, number> | undefined {
return undefined; // replace undefined with your code
}
// --- Build checks: these must all pass. Do not edit below this line. ---
assert.deepStrictEqual(
titles(activities),
[
"Intro to types",
"Union quiz",
"Narrowing drills",
"Interfaces deep dive",
"Generics quiz",
],
"titles(activities) should return an array of every activity's title, in order",
);
assert.strictEqual(
totalMinutes(activities),
100,
"totalMinutes(activities) should return the sum of every activity's minutes (100)",
);
assert.deepStrictEqual(
countByKind(activities),
{ lesson: 2, quiz: 2, practice: 1 },
"countByKind(activities) should map each kind to how many activities have it",
);
console.log("All checks passed.");
console.log("Titles:", titles(activities)?.join(", "));
console.log("Total minutes:", totalMinutes(activities));
console.log("By kind:", countByKind(activities));Expected output: All checks passed.
Titles: Intro to types, Union quiz, Narrowing drills, Interfaces deep dive, Generics quiz
Total minutes: 100
By kind: { lesson: 2, quiz: 2, practice: 1 }
Once it passes, try two variations and predict each before running:
- Push the whole activity. In
titles, pushactivityinstead ofactivity.title. Predict the shape of whattitles(activities)returns before running. You get an array of the full activity objects, not their titles, so thedeepStrictEqualagainst["Intro to types", ...]fails — a reminder that building a projection means pulling out the one field, not the whole record. - Drop the first-seen guard. In
countByKind, count with a barecounts[activity.kind] = counts[activity.kind] + 1, with no branch that starts an unseen kind at0. Predict whatcountByKind(activities)returns before running. The first time a kind is seen,counts[kind]isundefined, andundefined + 1isNaN— so every kind comes outNaN, showing why the count-up object needs an initial value.
Capstone milestone
Milestone — the data behind the domain model. The capstone tracks learning activities, and the very first layer is the shape of the thing it tracks: a typed collection of activity records, each with a title, a kind, and its own fields. This lesson's arrays-of-typed-objects are that exact data shape in miniature. Confirm you can model and read it.
Hint: You are not building the full domain model yet — the capstone sharpens the kind field into a discriminated union of lesson, quiz, and practice activities (you'll meet unions and narrowing in later lessons). Here you confirm the typed-collection foundation it rests on: an interface plus an array of records you can read and aggregate.
- Defined an interface describing the fields of one record
- Built an array of objects that all satisfy that interface
- Read a field off a record with dot notation
- Aggregated across the array (a count, a total, or a projection) with a loop
Key Takeaways
- Arrays store ordered collections of the same type
- Objects store key-value pairs for complex data
- Use type aliases and interfaces to define object shapes
- Tuples have fixed length and specific types for each position
- Enums define named constants
- Maps store key-value pairs with any key type
- Sets store unique values only
- Array methods like map, filter, and reduce are powerful tools
Pro Tip: Choose the right data structure for your needs. Arrays for ordered lists, objects for structured data, Maps for flexible key-value storage, and Sets for unique collections!
Next Steps
You've been using inline type annotations and basic interfaces to describe objects. Next, you'll dive deep into interfaces and type aliases — learning when to use each, how to extend them, and how intersection types let you compose complex shapes from simple ones.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.