Arrays and tuples are essential for working with collections of data in TypeScript. While arrays hold multiple values of the same type, tuples allow you to store a fixed number of elements with specific types in a specific order. Let's master both concepts!
Typed Arrays in TypeScript
Unlike JavaScript, TypeScript arrays are type-safe. You declare what type of elements the array should contain, and TypeScript ensures only those types are added.
JavaScript vs TypeScript Arrays
// JavaScript - No type safety
let numbers = [1, 2, 3];
// Can add any type - no errors
numbers.push(4);
numbers.push("five");
numbers.push(true);
numbers.push({ value: 6 });
// Runtime errors only
let sum = numbers.reduce((a, b) => a + b);
// ❌ Crashes: Cannot add string to number!// TypeScript - Type-safe arrays
let numbers: number[] = [1, 2, 3];
// Can only add numbers
numbers.push(4); // ✅ Works
numbers.push("five"); // ❌ Error: string not assignable
numbers.push(true); // ❌ Error: boolean not assignable
numbers.push({ value: 6 }); // ❌ Error: object not assignable
// Safe operations
let sum = numbers.reduce((a, b) => a + b);
// ✅ TypeScript knows this will work!Why TypeScript is Better: TypeScript ensures arrays contain only the specified type, preventing mixed-type bugs and runtime errors.
Key TypeScript Benefits
- Type safety catches errors before runtime
- Better IDE autocomplete and IntelliSense
- Self-documenting code through types
- Easier refactoring and maintenance
- Fewer runtime errors in production
Array Type Syntax
There are two ways to declare array types in TypeScript:
// Method 1: Type[] syntax (recommended)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let flags: boolean[] = [true, false, true];
// Method 2: Array<Type> syntax (generic syntax)
let scores: Array<number> = [85, 90, 78, 92];
let cities: Array<string> = ["Lagos", "Abuja", "Kano"];
let active: Array<boolean> = [true, true, false];
// Both syntaxes are equivalent, but Type[] is more common
let fruits: string[] = ["Apple", "Banana", "Orange"];
console.log(numbers); // [1, 2, 3, 4, 5]
console.log(names); // ["Alice", "Bob", "Charlie"]💡 Array Syntax Preference
Use Type[] syntax (e.g., string[]) for simple arrays. It's more concise and readable. Use Array<Type> for complex types or when you need multiple type parameters.
Creating and Initializing Arrays
Different Ways to Create Arrays
// Empty array with type annotation
let emptyNumbers: number[] = [];
let emptyStrings: string[] = [];
// Array with initial values
let colors: string[] = ["Red", "Green", "Blue"];
let primes: number[] = [2, 3, 5, 7, 11];
// Using Array constructor
let zeroes: number[] = new Array(5).fill(0); // [0, 0, 0, 0, 0]
let range: number[] = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]
// Type inference - TypeScript infers array type
let inferredNumbers = [1, 2, 3]; // Type: number[]
let inferredStrings = ["a", "b"]; // Type: string[]
let inferredMixed = [1, "two", true]; // Type: (string | number | boolean)[]
// Explicitly typed for clarity
let studentNames: string[] = ["Yusuf", "Amina", "Chidi"];
let testScores: number[] = [85, 92, 78, 95, 88];
console.log(`Students: ${studentNames.join(", ")}`);
console.log(`Scores: ${testScores.join(", ")}`);Mixed Type Arrays (Union Types)
// Array that can contain strings OR numbers
let mixedArray: (string | number)[] = [1, "two", 3, "four"];
// Array that can contain strings OR numbers OR booleans
let flexibleArray: (string | number | boolean)[] = [
"hello",
42,
true,
"world",
100
];
// Adding elements must match union type
mixedArray.push(5); // ✅ Works (number)
mixedArray.push("six"); // ✅ Works (string)
// mixedArray.push(true); // ❌ Error: boolean not in union
// Practical example: API response data
type ResponseData = (string | number | null)[];
let apiData: ResponseData = ["success", 200, null, "message"];
console.log(mixedArray); // [1, "two", 3, "four", 5, "six"]
console.log(apiData); // ["success", 200, null, "message"]Common Array Operations
Adding and Removing Elements
let fruits: string[] = ["Apple", "Banana"];
// Add to end
fruits.push("Orange"); // ["Apple", "Banana", "Orange"]
fruits.push("Mango", "Grape"); // Add multiple
// Add to beginning
fruits.unshift("Strawberry"); // ["Strawberry", "Apple", "Banana", "Orange", "Mango", "Grape"]
// Remove from end
let lastFruit: string | undefined = fruits.pop(); // "Grape"
// Remove from beginning
let firstFruit: string | undefined = fruits.shift(); // "Strawberry"
// Remove/Add at specific position (splice)
fruits.splice(1, 1); // Remove 1 item at index 1
fruits.splice(1, 0, "Pineapple"); // Add at index 1
fruits.splice(1, 1, "Watermelon"); // Replace at index 1
// Create new array without modifying original
let moreFruits: string[] = fruits.concat(["Lemon", "Lime"]);
let sliced: string[] = fruits.slice(1, 3); // Get elements from index 1 to 3
console.log(fruits); // Current state
console.log(moreFruits); // Original + new items
console.log(sliced); // Subset of arrayAccessing and Searching Elements
let numbers: number[] = [10, 20, 30, 40, 50];
// Access by index
let first: number = numbers[0]; // 10
let last: number = numbers[numbers.length - 1]; // 50
// Find index of element
let index: number = numbers.indexOf(30); // 2
let notFound: number = numbers.indexOf(99); // -1
// Check if element exists
let hasThirty: boolean = numbers.includes(30); // true
let hasSixty: boolean = numbers.includes(60); // false
// Find element that matches condition
let found: number | undefined = numbers.find(num => num > 25); // 30
let foundIndex: number = numbers.findIndex(num => num > 25); // 2
// Check if all/some elements match condition
let allPositive: boolean = numbers.every(num => num > 0); // true
let someAbove40: boolean = numbers.some(num => num > 40); // true
console.log(`First: ${first}, Last: ${last}`);
console.log(`Index of 30: ${index}`);
console.log(`Has 30: ${hasThirty}`);
console.log(`Found: ${found}`);Transforming Arrays
let numbers: number[] = [1, 2, 3, 4, 5];
// Map - Transform each element
let doubled: number[] = numbers.map(num => num * 2);
// [2, 4, 6, 8, 10]
let squared: number[] = numbers.map(num => num ** 2);
// [1, 4, 9, 16, 25]
// Filter - Keep elements that match condition
let evens: number[] = numbers.filter(num => num % 2 === 0);
// [2, 4]
let greaterThanThree: number[] = numbers.filter(num => num > 3);
// [4, 5]
// Reduce - Combine all elements into single value
let sum: number = numbers.reduce((total, num) => total + num, 0);
// 15
let product: number = numbers.reduce((total, num) => total * num, 1);
// 120
// Sort - Arrange elements
let unsorted: number[] = [5, 2, 8, 1, 9];
let sorted: number[] = [...unsorted].sort((a, b) => a - b);
// [1, 2, 5, 8, 9]
let descending: number[] = [...unsorted].sort((a, b) => b - a);
// [9, 8, 5, 2, 1]
// Reverse - Reverse order
let reversed: number[] = [...numbers].reverse();
// [5, 4, 3, 2, 1]
console.log(`Original: ${numbers}`);
console.log(`Doubled: ${doubled}`);
console.log(`Evens: ${evens}`);
console.log(`Sum: ${sum}`);⚠️ Important: Array Method Types
Methods like map, filter, and find preserve or return appropriate types. map can change the type, filter keeps the same type, and find returns Type | undefined.
Iterating Over Arrays
let fruits: string[] = ["Apple", "Banana", "Orange", "Mango"];
// For loop
for (let i = 0; i < fruits.length; i++) {
console.log(`${i + 1}. ${fruits[i]}`);
}
// For...of loop (recommended for values)
for (const fruit of fruits) {
console.log(`Fruit: ${fruit}`);
}
// For...in loop (for indices, less common)
for (const index in fruits) {
console.log(`Index ${index}: ${fruits[index]}`);
}
// forEach method
fruits.forEach((fruit, index) => {
console.log(`${index + 1}. ${fruit}`);
});
// forEach with type safety
fruits.forEach((fruit: string, index: number, array: string[]) => {
console.log(`Processing ${fruit} at index ${index}`);
});
// While loop
let i: number = 0;
while (i < fruits.length) {
console.log(fruits[i]);
i++;
}
// Map for transformation while iterating
let upperFruits: string[] = fruits.map(fruit => fruit.toUpperCase());
console.log(upperFruits); // ["APPLE", "BANANA", "ORANGE", "MANGO"]Best Practice: Use for...of when you need the values, forEach when you need both value and index, and map when transforming to a new array.
Multidimensional Arrays
Arrays can contain other arrays, creating 2D arrays (matrices), 3D arrays, and beyond.
// 2D Array (Matrix)
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Accessing 2D array elements
let firstRow: number[] = matrix[0]; // [1, 2, 3]
let element: number = matrix[1][2]; // 6 (row 1, col 2)
// Iterating 2D array
for (let row of matrix) {
for (let num of row) {
console.log(num);
}
}
// Practical example: Student grades
let grades: number[][] = [
[85, 90, 78], // Student 1 grades
[92, 88, 95], // Student 2 grades
[76, 82, 79] // Student 3 grades
];
// Calculate average for each student
let averages: number[] = grades.map(studentGrades => {
let sum = studentGrades.reduce((a, b) => a + b, 0);
return sum / studentGrades.length;
});
console.log(`Averages: ${averages}`); // [84.33, 91.67, 79]
// 3D Array example
let cube: number[][][] = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
];
console.log(cube[0][1][0]); // 3
// String 2D array - Board game
let ticTacToe: string[][] = [
["X", "O", "X"],
["O", "X", "O"],
["O", "X", "X"]
];
console.log(ticTacToe);Practical Example: Seating Chart
// Cinema seating arrangement
type Seat = "available" | "booked" | "reserved";
let cinema: Seat[][] = [
["available", "available", "booked", "available"],
["reserved", "available", "available", "available"],
["available", "booked", "booked", "available"],
["available", "available", "available", "reserved"]
];
// Function to book a seat
function bookSeat(row: number, col: number): boolean {
if (cinema[row][col] === "available") {
cinema[row][col] = "booked";
return true;
}
return false;
}
// Function to count available seats
function countAvailableSeats(): number {
let count = 0;
for (let row of cinema) {
for (let seat of row) {
if (seat === "available") {
count++;
}
}
}
return count;
}
bookSeat(0, 0); // Book first seat
console.log(`Available seats: ${countAvailableSeats()}`);Readonly Arrays
Readonly arrays cannot be modified after creation. This is useful for data that shouldn't change.
// Readonly array - cannot be modified
let readonlyNumbers: readonly number[] = [1, 2, 3, 4, 5];
// These operations are not allowed:
// readonlyNumbers.push(6); // ❌ Error: push doesn't exist on readonly
// readonlyNumbers.pop(); // ❌ Error: pop doesn't exist on readonly
// readonlyNumbers[0] = 10; // ❌ Error: cannot assign to readonly
// Reading is allowed
let first: number = readonlyNumbers[0]; // ✅ Works
let sliced = readonlyNumbers.slice(1, 3); // ✅ Works (creates new array)
// Alternative syntax using ReadonlyArray
let readonlyStrings: ReadonlyArray<string> = ["a", "b", "c"];
// Practical use: Configuration
const DAYS_OF_WEEK: readonly string[] = [
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"
];
// DAYS_OF_WEEK.push("Extraday"); // ❌ Error: cannot modify
// Function that doesn't modify array
function displayArray(arr: readonly number[]): void {
// arr.push(99); // ❌ Error: cannot modify
console.log(arr.join(", ")); // ✅ Reading is fine
}
// Converting readonly to mutable
let mutableCopy: number[] = [...readonlyNumbers];
mutableCopy.push(6); // ✅ Works on copy
console.log(`Readonly: ${readonlyNumbers}`);
console.log(`Mutable copy: ${mutableCopy}`);🔒 When to Use Readonly Arrays
- Constants that should never change (days of week, months)
- Configuration data
- Function parameters you don't want to modify
- Preventing accidental mutations
Tuples in TypeScript
Tuples are arrays with a fixed length and specific types in a specific order. Unlike regular arrays where all elements are the same type, tuples can have different types for each position.
Basic Tuple Syntax
// Tuple: [string, number] - exactly 2 elements
let person: [string, number] = ["Yusuf", 25];
// Accessing tuple elements
let name: string = person[0]; // "Yusuf"
let age: number = person[1]; // 25
// Tuples enforce order and type
let point: [number, number] = [10, 20]; // x, y coordinates
// This works
let validTuple: [string, number] = ["Alice", 30];
// These cause errors:
// let wrong1: [string, number] = [30, "Alice"]; // ❌ Wrong order
// let wrong2: [string, number] = ["Alice"]; // ❌ Too few elements
// let wrong3: [string, number] = ["Alice", 30, true]; // ❌ Too many
// Tuple with different types
let rgb: [number, number, number] = [255, 128, 0];
let rgba: [number, number, number, number] = [255, 128, 0, 1];
console.log(`Person: ${person[0]}, Age: ${person[1]}`);
console.log(`RGB: rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`);Tuple vs Array
// Array - same type, any length
let numbers: number[] = [1, 2, 3, 4, 5];
numbers.push(6); // Can add more
// Tuple - specific types, fixed length
let coordinate: [number, number] = [10, 20];
// coordinate.push(30); // TypeScript allows this (limitation)
// But accessing coordinate[2] gives error
// When to use Array
let studentGrades: number[] = [85, 90, 78, 92]; // Varying length
// When to use Tuple
let studentRecord: [string, number, boolean] = ["Yusuf", 85, true];
// [name, grade, passed]
// Array: flexible, uniform type
// Tuple: fixed structure, varied typesTuple with Optional Elements
// Optional tuple elements use ?
let optionalTuple: [string, number?] = ["Hello"]; // number is optional
optionalTuple = ["Hello", 42]; // Also valid
// HTTP response: [status code, message?, data?]
let response1: [number, string?, any?] = [200];
let response2: [number, string?, any?] = [200, "Success"];
let response3: [number, string?, any?] = [200, "Success", { id: 1 }];
// Optional elements must come after required ones
// let invalid: [number?, string] = [undefined, "text"]; // ❌ Error
console.log(response1); // [200]
console.log(response2); // [200, "Success"]
console.log(response3); // [200, "Success", { id: 1 }]Tuple with Rest Elements
// Tuple with rest elements (variable length at end)
let tuple1: [string, ...number[]] = ["Numbers:", 1, 2, 3, 4, 5];
let tuple2: [string, ...number[]] = ["Count:", 42];
// First element is string, rest are numbers
console.log(tuple1); // ["Numbers:", 1, 2, 3, 4, 5]
// Practical example: Logger
type LogEntry = [string, Date, ...string[]];
let log1: LogEntry = ["INFO", new Date(), "User logged in"];
let log2: LogEntry = ["ERROR", new Date(), "Database error", "Connection failed"];
function logMessage(entry: LogEntry): void {
let [level, timestamp, ...messages] = entry;
console.log(`[${level}] ${timestamp.toISOString()}: ${messages.join(" - ")}`);
}
logMessage(log1);
logMessage(log2);Readonly Tuples
// Readonly tuple - cannot be modified
let readonlyTuple: readonly [string, number] = ["Yusuf", 25];
// These are not allowed:
// readonlyTuple[0] = "Ahmed"; // ❌ Error: cannot assign
// readonlyTuple.push("extra"); // ❌ Error: push doesn't exist
// Reading is allowed
let tupleName: string = readonlyTuple[0]; // ✅ Works
// Practical use: Constants
const ORIGIN: readonly [number, number] = [0, 0];
const API_VERSION: readonly [number, number, number] = [1, 0, 0];
console.log(`Origin: (${ORIGIN[0]}, ${ORIGIN[1]})`);
console.log(`API Version: ${API_VERSION.join(".")}`);Destructuring Tuples
// Tuple destructuring
let person: [string, number, string] = ["Yusuf Ibrahim", 25, "Lagos"];
// Destructure into variables
let [fullName, age, city] = person;
console.log(fullName); // "Yusuf Ibrahim"
console.log(age); // 25
console.log(city); // "Lagos"
// Skip elements
let [name, , location] = person; // Skip age
console.log(`${name} from ${location}`);
// Function returning tuple
function getCoordinates(): [number, number] {
return [10.5, 20.3];
}
let [x, y] = getCoordinates();
console.log(`X: ${x}, Y: ${y}`);
// Swap values using tuple destructuring
let a = 5;
let b = 10;
[a, b] = [b, a]; // Swap
console.log(`a: ${a}, b: ${b}`); // a: 10, b: 5Practical Examples
Example 1: Shopping Cart
// Product: [name, price, quantity]
type CartItem = [string, number, number];
let cart: CartItem[] = [
["Laptop", 250000, 1],
["Mouse", 5000, 2],
["Keyboard", 15000, 1],
["Monitor", 80000, 1]
];
// Calculate total
function calculateTotal(items: CartItem[]): number {
return items.reduce((total, [_, price, quantity]) => {
return total + (price * quantity);
}, 0);
}
// Display cart
function displayCart(items: CartItem[]): void {
console.log("=== Shopping Cart ===");
items.forEach(([name, price, qty]) => {
console.log(`${name}: ₦${price} x ${qty} = ₦${price * qty}`);
});
console.log(`Total: ₦${calculateTotal(items)}`);
}
displayCart(cart);
// Output:
// === Shopping Cart ===
// Laptop: ₦250000 x 1 = ₦250000
// Mouse: ₦5000 x 2 = ₦10000
// Keyboard: ₦15000 x 1 = ₦15000
// Monitor: ₦80000 x 1 = ₦80000
// Total: ₦355000Example 2: Student Management
// Student data: [ID, Name, Grades]
type Student = [number, string, number[]];
let students: Student[] = [
[1, "Yusuf Ibrahim", [85, 90, 78, 92, 88]],
[2, "Amina Mohammed", [95, 98, 92, 96, 94]],
[3, "Chidi Okafor", [76, 82, 79, 85, 80]]
];
// Calculate average grade
function getAverage(grades: number[]): number {
const sum = grades.reduce((a, b) => a + b, 0);
return sum / grades.length;
}
// Generate report
function generateReport(students: Student[]): void {
console.log("=== Student Report ===\n");
students.forEach(([id, name, grades]) => {
const average = getAverage(grades);
const grade = average >= 90 ? "A" : average >= 80 ? "B" :
average >= 70 ? "C" : average >= 60 ? "D" : "F";
console.log(`ID: ${id}`);
console.log(`Name: ${name}`);
console.log(`Grades: ${grades.join(", ")}`);
console.log(`Average: ${average.toFixed(2)}`);
console.log(`Grade: ${grade}`);
console.log();
});
}
generateReport(students);Example 3: Database Query Results
// Database row: [id, name, email, active, createdAt]
type UserRow = [number, string, string, boolean, Date];
let queryResults: UserRow[] = [
[1, "Yusuf Ibrahim", "yusuf@example.com", true, new Date("2024-01-15")],
[2, "Amina Hassan", "amina@example.com", true, new Date("2024-02-20")],
[3, "Emeka Eze", "emeka@example.com", false, new Date("2024-03-10")]
];
// Filter active users
function getActiveUsers(users: UserRow[]): UserRow[] {
return users.filter(([_, __, ___, isActive]) => isActive);
}
// Format user display
function displayUsers(users: UserRow[]): void {
users.forEach(([id, name, email, active, created]) => {
const status = active ? "Active" : "Inactive";
const date = created.toLocaleDateString();
console.log(`[${id}] ${name} (${email}) - ${status} - Joined: ${date}`);
});
}
const activeUsers = getActiveUsers(queryResults);
console.log("Active Users:");
displayUsers(activeUsers);Test Your Knowledge
Check your understanding of arrays and tuples:
Type Safety Check
Which array declaration is valid in TypeScript?
// Option A
let names: string[] = ["John", "Jane", 123];
// Option B
let ages: number[] = [25, 30, 35];
// Option C
let mixed: any[] = [1, "two", true];
// Option D
let values: string[] = [1, 2, 3];Common TypeScript Error
// Trying to mix types in a typed array
let scores: number[] = [85, 90, 78];
// Attempting to add wrong types
scores.push(92); // Works fine
scores.push("95"); // Error!
scores.push(true); // Error!
// Attempting wrong array methods
let result = scores.toUpperCase(); // Error!❌ Argument of type 'string' is not assignable to parameter of type 'number'
What's Wrong?
TypeScript prevents you from adding elements of the wrong type to an array, and from calling methods that don't exist on that array type.
// Type-safe array operations
let scores: number[] = [85, 90, 78];
// ✅ Only add numbers
scores.push(92);
scores.push(95); // Convert string to number if needed
// ✅ Use appropriate methods for number arrays
let average = scores.reduce((a, b) => a + b, 0) / scores.length;
let highest = Math.max(...scores);
console.log(`Average: ${average}`);
console.log(`Highest: ${highest}`);Solution: Ensure all array elements match the declared type
Best Practices
- Use specific array types instead of
any[] - Prefer readonly arrays for data that shouldn't change
- Use tuples for fixed-length, varied-type data structures
- Use arrays for flexible-length, uniform-type collections
- Destructure tuples for cleaner code
- Use array methods (map, filter, reduce) for transformations
- Spread operator to avoid modifying original arrays
- Type array method results explicitly when TypeScript can't infer
// ✅ Good practices
// 1. Specific types
let userIds: number[] = [1, 2, 3];
let userNames: string[] = ["Alice", "Bob"];
// 2. Readonly for constants
const MONTHS: readonly string[] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
// 3. Tuples for structured data
type DatabaseRow = [number, string, boolean];
let user: DatabaseRow = [1, "Yusuf", true];
// 4. Destructuring for clarity
let [id, name, active] = user;
// 5. Non-mutating operations
let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map(n => n * 2); // Creates new array
let evens = numbers.filter(n => n % 2 === 0); // Creates new array
// 6. Spread to copy
let copy = [...numbers];
copy.push(6); // Original unchanged
// ❌ Avoid
let anyArray: any[] = [1, "two", true]; // No type safety
numbers.push(...[6, 7, 8]); // Mutates original
let unsafeAccess = numbers[100]; // undefined, no errorKey Takeaways
- TypeScript arrays are type-safe collections:
Type[]orArray<Type> - Array methods (map, filter, reduce) preserve type safety
- Use
readonlyarrays for immutable data - Tuples have fixed length and specific types per position
- Destructuring makes working with tuples cleaner
- Use arrays for flexible collections, tuples for structured data
- Multidimensional arrays create matrices and complex structures
- TypeScript prevents adding wrong types to typed arrays
- Optional and rest elements work with tuples
- Spread operator creates copies without mutation
What's Next?
Now that you've mastered arrays and tuples, you're ready to explore Objects and Type Aliases! In the next lesson, you'll learn how to define complex object shapes, create reusable type aliases, and work with object properties. These concepts build on what you learned about primitive types and collections.
Get ready to structure your data with TypeScript's powerful object typing features!