Lesson 5.2: Data Transformation
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Transform arrays using map() to change data shape
- Filter data using filter() with complex conditions
- Aggregate data using reduce() for calculations and grouping
- Chain multiple transformations together
- Choose the right method for different data operations
Introduction
When you fetch data from an API, it rarely comes in exactly the format you need. You might need to extract specific fields, filter out irrelevant items, or calculate totals. JavaScript provides powerful array methods for these transformations: map, filter, and reduce.
// Raw API data
const apiUsers = [
{ id: 1, first_name: 'Alice', last_name: 'Smith', role: 'admin', active: true },
{ id: 2, first_name: 'Bob', last_name: 'Jones', role: 'user', active: false },
{ id: 3, first_name: 'Carol', last_name: 'White', role: 'user', active: true },
];
// Transform to what your app needs
const activeUserNames = apiUsers
.filter((user) => user.active)
.map((user) => `${user.first_name} ${user.last_name}`);
// ["Alice Smith", "Carol White"]
map(): Transform Each Element
map() creates a new array by applying a function to every element.
Basic Usage
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
const squared = numbers.map((n) => n * n);
console.log(squared); // [1, 4, 9, 16, 25]
Transforming Object Shape
interface ApiUser {
id: number;
first_name: string;
last_name: string;
email_address: string;
}
interface AppUser {
id: number;
fullName: string;
email: string;
}
const apiUsers: ApiUser[] = [
{ id: 1, first_name: 'Alice', last_name: 'Smith', email_address: 'alice@example.com' },
{ id: 2, first_name: 'Bob', last_name: 'Jones', email_address: 'bob@example.com' },
];
const appUsers: AppUser[] = apiUsers.map((user) => ({
id: user.id,
fullName: `${user.first_name} ${user.last_name}`,
email: user.email_address,
}));
console.log(appUsers);
// [
// { id: 1, fullName: "Alice Smith", email: "alice@example.com" },
// { id: 2, fullName: "Bob Jones", email: "bob@example.com" }
// ]
Extracting Single Property
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 },
];
// Extract just the names
const names = products.map((p) => p.name);
console.log(names); // ["Laptop", "Mouse", "Keyboard"]
// Extract just the IDs
const ids = products.map((p) => p.id);
console.log(ids); // [1, 2, 3]
map() with Index
const letters = ['a', 'b', 'c'];
const indexed = letters.map((letter, index) => ({
position: index + 1,
letter: letter.toUpperCase(),
}));
console.log(indexed);
// [
// { position: 1, letter: "A" },
// { position: 2, letter: "B" },
// { position: 3, letter: "C" }
// ]
filter(): Select Elements
filter() creates a new array with elements that pass a test.
Basic Usage
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
const greaterThanFive = numbers.filter((n) => n > 5);
console.log(greaterThanFive); // [6, 7, 8, 9, 10]
Filtering Objects
interface Task {
id: number;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
}
const tasks: Task[] = [
{ id: 1, title: 'Write report', completed: false, priority: 'high' },
{ id: 2, title: 'Review PR', completed: true, priority: 'medium' },
{ id: 3, title: 'Update docs', completed: false, priority: 'low' },
{ id: 4, title: 'Fix bug', completed: false, priority: 'high' },
];
// Get incomplete tasks
const incomplete = tasks.filter((task) => !task.completed);
console.log(incomplete.length); // 3
// Get high priority tasks
const highPriority = tasks.filter((task) => task.priority === 'high');
console.log(highPriority.map((t) => t.title)); // ["Write report", "Fix bug"]
// Multiple conditions
const urgentIncomplete = tasks.filter((task) => !task.completed && task.priority === 'high');
console.log(urgentIncomplete.map((t) => t.title)); // ["Write report", "Fix bug"]
Filtering with Type Guards
// Remove null/undefined values
const mixed: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c'];
const strings = mixed.filter((item): item is string => item !== null && item !== undefined);
console.log(strings); // ["a", "b", "c"]
// Filter by type
interface Dog {
type: 'dog';
bark: () => void;
}
interface Cat {
type: 'cat';
meow: () => void;
}
type Animal = Dog | Cat;
const animals: Animal[] = [
{ type: 'dog', bark: () => console.log('Woof!') },
{ type: 'cat', meow: () => console.log('Meow!') },
{ type: 'dog', bark: () => console.log('Bark!') },
];
const dogs = animals.filter((animal): animal is Dog => animal.type === 'dog');
dogs.forEach((dog) => dog.bark()); // TypeScript knows these are Dogs
Search/Filter by Text
interface Article {
id: number;
title: string;
content: string;
tags: string[];
}
const articles: Article[] = [
{
id: 1,
title: 'TypeScript Basics',
content: 'Learn TypeScript...',
tags: ['typescript', 'beginner'],
},
{
id: 2,
title: 'Advanced Patterns',
content: 'Explore patterns...',
tags: ['patterns', 'advanced'],
},
{ id: 3, title: 'TypeScript Tips', content: 'Useful tips...', tags: ['typescript', 'tips'] },
];
function searchArticles(query: string): Article[] {
const lowerQuery = query.toLowerCase();
return articles.filter(
(article) =>
article.title.toLowerCase().includes(lowerQuery) ||
article.content.toLowerCase().includes(lowerQuery) ||
article.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
);
}
console.log(searchArticles('typescript').map((a) => a.title));
// ["TypeScript Basics", "TypeScript Tips"]
reduce(): Aggregate to Single Value
reduce() processes all elements to produce a single result (number, object, array, etc.).
Basic Accumulation
const numbers = [1, 2, 3, 4, 5];
// Sum all numbers
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // 15
// Multiply all numbers
const product = numbers.reduce((result, num) => result * num, 1);
console.log(product); // 120
// Find maximum
const max = numbers.reduce((biggest, num) => (num > biggest ? num : biggest), numbers[0]);
console.log(max); // 5
Understanding reduce() Parameters
array.reduce((accumulator, currentValue, currentIndex, array) => {
// Return updated accumulator
}, initialValue);
// Example with all parameters
const numbers = [1, 2, 3];
numbers.reduce((acc, val, idx, arr) => {
console.log(`Index ${idx}: acc=${acc}, val=${val}, arr=[${arr}]`);
return acc + val;
}, 0);
// Index 0: acc=0, val=1, arr=[1,2,3]
// Index 1: acc=1, val=2, arr=[1,2,3]
// Index 2: acc=3, val=3, arr=[1,2,3]
// Result: 6
Counting Occurrences
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const counts = fruits.reduce(
(acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
console.log(counts);
// { apple: 3, banana: 2, orange: 1 }
Grouping Data
interface Transaction {
id: number;
category: string;
amount: number;
}
const transactions: Transaction[] = [
{ id: 1, category: 'food', amount: 50 },
{ id: 2, category: 'transport', amount: 30 },
{ id: 3, category: 'food', amount: 25 },
{ id: 4, category: 'entertainment', amount: 100 },
{ id: 5, category: 'food', amount: 40 },
];
// Group by category
const byCategory = transactions.reduce(
(groups, transaction) => {
const category = transaction.category;
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(transaction);
return groups;
},
{} as Record<string, Transaction[]>
);
console.log(byCategory);
// {
// food: [{ id: 1, ... }, { id: 3, ... }, { id: 5, ... }],
// transport: [{ id: 2, ... }],
// entertainment: [{ id: 4, ... }]
// }
// Sum by category
const totalsByCategory = transactions.reduce(
(totals, transaction) => {
const category = transaction.category;
totals[category] = (totals[category] || 0) + transaction.amount;
return totals;
},
{} as Record<string, number>
);
console.log(totalsByCategory);
// { food: 115, transport: 30, entertainment: 100 }
Building Objects from Arrays
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Carol', email: 'carol@example.com' },
];
// Create lookup map by ID
const usersById = users.reduce(
(map, user) => {
map[user.id] = user;
return map;
},
{} as Record<number, User>
);
console.log(usersById[2].name); // "Bob"
// Create email to user map
const usersByEmail = users.reduce(
(map, user) => {
map[user.email] = user;
return map;
},
{} as Record<string, User>
);
console.log(usersByEmail['alice@example.com'].name); // "Alice"
Flattening Arrays
const nested = [
[1, 2],
[3, 4],
[5, 6],
];
const flat = nested.reduce((acc, arr) => [...acc, ...arr], [] as number[]);
console.log(flat); // [1, 2, 3, 4, 5, 6]
// Or use flat() method (simpler)
const flatSimple = nested.flat();
console.log(flatSimple); // [1, 2, 3, 4, 5, 6]
Chaining Methods
The real power comes from combining these methods.
Filter then Map
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
const products: Product[] = [
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Phone', price: 699, inStock: false },
{ id: 3, name: 'Tablet', price: 499, inStock: true },
{ id: 4, name: 'Watch', price: 299, inStock: true },
];
// Get names of available products under $500
const affordableAvailable = products.filter((p) => p.inStock && p.price < 500).map((p) => p.name);
console.log(affordableAvailable); // ["Tablet", "Watch"]
Map, Filter, Reduce
interface Order {
id: number;
items: { name: string; price: number; quantity: number }[];
status: 'pending' | 'completed' | 'cancelled';
}
const orders: Order[] = [
{
id: 1,
items: [
{ name: 'Book', price: 15, quantity: 2 },
{ name: 'Pen', price: 2, quantity: 5 },
],
status: 'completed',
},
{
id: 2,
items: [{ name: 'Notebook', price: 8, quantity: 3 }],
status: 'cancelled',
},
{
id: 3,
items: [
{ name: 'Laptop', price: 999, quantity: 1 },
{ name: 'Mouse', price: 29, quantity: 1 },
],
status: 'completed',
},
];
// Calculate total revenue from completed orders
const totalRevenue = orders
.filter((order) => order.status === 'completed')
.map((order) => order.items.reduce((sum, item) => sum + item.price * item.quantity, 0))
.reduce((total, orderTotal) => total + orderTotal, 0);
console.log(totalRevenue); // 40 + 1028 = 1068
Complex Data Pipeline
interface RawUser {
id: number;
name: string;
email: string;
department: string;
salary: number;
hireDate: string;
isActive: boolean;
}
const employees: RawUser[] = [
{
id: 1,
name: 'Alice',
email: 'alice@co.com',
department: 'Engineering',
salary: 95000,
hireDate: '2020-03-15',
isActive: true,
},
{
id: 2,
name: 'Bob',
email: 'bob@co.com',
department: 'Marketing',
salary: 75000,
hireDate: '2019-06-01',
isActive: true,
},
{
id: 3,
name: 'Carol',
email: 'carol@co.com',
department: 'Engineering',
salary: 105000,
hireDate: '2018-01-10',
isActive: true,
},
{
id: 4,
name: 'Dave',
email: 'dave@co.com',
department: 'Engineering',
salary: 85000,
hireDate: '2021-09-20',
isActive: false,
},
{
id: 5,
name: 'Eve',
email: 'eve@co.com',
department: 'Marketing',
salary: 80000,
hireDate: '2020-11-05',
isActive: true,
},
];
// Get department salary report for active employees
const departmentReport = employees
.filter((emp) => emp.isActive)
.reduce(
(report, emp) => {
if (!report[emp.department]) {
report[emp.department] = {
employees: [],
totalSalary: 0,
averageSalary: 0,
};
}
report[emp.department].employees.push(emp.name);
report[emp.department].totalSalary += emp.salary;
return report;
},
{} as Record<string, { employees: string[]; totalSalary: number; averageSalary: number }>
);
// Calculate averages
Object.values(departmentReport).forEach((dept) => {
dept.averageSalary = dept.totalSalary / dept.employees.length;
});
console.log(departmentReport);
// {
// Engineering: { employees: ["Alice", "Carol"], totalSalary: 200000, averageSalary: 100000 },
// Marketing: { employees: ["Bob", "Eve"], totalSalary: 155000, averageSalary: 77500 }
// }
Other Useful Array Methods
find() - Get First Match
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Carol' },
];
const bob = users.find((u) => u.name === 'Bob');
console.log(bob); // { id: 2, name: "Bob" }
const dave = users.find((u) => u.name === 'Dave');
console.log(dave); // undefined
findIndex() - Get First Match Index
const numbers = [10, 20, 30, 40, 50];
const index = numbers.findIndex((n) => n > 25);
console.log(index); // 2 (index of 30)
some() - Check If Any Match
const numbers = [1, 2, 3, 4, 5];
const hasEven = numbers.some((n) => n % 2 === 0);
console.log(hasEven); // true
const hasNegative = numbers.some((n) => n < 0);
console.log(hasNegative); // false
every() - Check If All Match
const numbers = [2, 4, 6, 8, 10];
const allEven = numbers.every((n) => n % 2 === 0);
console.log(allEven); // true
const allPositive = numbers.every((n) => n > 0);
console.log(allPositive); // true
includes() - Check Presence
const fruits = ['apple', 'banana', 'orange'];
console.log(fruits.includes('banana')); // true
console.log(fruits.includes('grape')); // false
Performance Considerations
Avoid Multiple Passes When Possible
// Less efficient: 3 iterations
const result1 = data
.filter((x) => x.active)
.map((x) => x.value)
.reduce((sum, v) => sum + v, 0);
// More efficient: 1 iteration
const result2 = data.reduce((sum, x) => {
if (x.active) {
return sum + x.value;
}
return sum;
}, 0);
Use for...of for Early Exit
// find() stops at first match
const found = array.find((x) => x.id === targetId);
// If you need more control:
function findWithLogging<T>(array: T[], predicate: (item: T) => boolean): T | undefined {
for (const item of array) {
console.log('Checking:', item);
if (predicate(item)) {
return item;
}
}
return undefined;
}
Exercises
Exercise 1: Transform API Data
Transform this API response into the desired format:
interface ApiProduct {
product_id: number;
product_name: string;
price_cents: number;
is_available: boolean;
}
interface DisplayProduct {
id: number;
name: string;
price: string; // Formatted as "$X.XX"
}
const apiProducts: ApiProduct[] = [
{ product_id: 1, product_name: 'Laptop', price_cents: 99900, is_available: true },
{ product_id: 2, product_name: 'Mouse', price_cents: 2999, is_available: false },
{ product_id: 3, product_name: 'Keyboard', price_cents: 7999, is_available: true },
];
// Transform to DisplayProduct[], only including available items
Solution
const displayProducts: DisplayProduct[] = apiProducts
.filter((p) => p.is_available)
.map((p) => ({
id: p.product_id,
name: p.product_name,
price: `$${(p.price_cents / 100).toFixed(2)}`,
}));
console.log(displayProducts);
// [
// { id: 1, name: "Laptop", price: "$999.00" },
// { id: 3, name: "Keyboard", price: "$79.99" }
// ]
Exercise 2: Group and Aggregate
Group these sales by month and calculate monthly totals:
interface Sale {
date: string; // "YYYY-MM-DD"
amount: number;
product: string;
}
const sales: Sale[] = [
{ date: '2024-01-15', amount: 100, product: 'A' },
{ date: '2024-01-20', amount: 150, product: 'B' },
{ date: '2024-02-05', amount: 200, product: 'A' },
{ date: '2024-02-18', amount: 75, product: 'C' },
{ date: '2024-03-01', amount: 300, product: 'B' },
];
// Expected output:
// { "2024-01": 250, "2024-02": 275, "2024-03": 300 }
Solution
const monthlyTotals = sales.reduce(
(totals, sale) => {
const month = sale.date.substring(0, 7); // "YYYY-MM"
totals[month] = (totals[month] || 0) + sale.amount;
return totals;
},
{} as Record<string, number>
);
console.log(monthlyTotals);
// { "2024-01": 250, "2024-02": 275, "2024-03": 300 }
Exercise 3: Build a Search Function
Create a function that searches users by multiple criteria:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
active: boolean;
}
interface SearchCriteria {
query?: string; // Search in name and email
role?: User['role']; // Filter by role
activeOnly?: boolean; // Only active users
}
const users: User[] = [
{ id: 1, name: 'Alice Admin', email: 'alice@example.com', role: 'admin', active: true },
{ id: 2, name: 'Bob User', email: 'bob@example.com', role: 'user', active: true },
{ id: 3, name: 'Carol Guest', email: 'carol@example.com', role: 'guest', active: false },
{ id: 4, name: 'Dave User', email: 'dave@example.com', role: 'user', active: true },
];
function searchUsers(criteria: SearchCriteria): User[] {
// Your implementation
}
Solution
function searchUsers(criteria: SearchCriteria): User[] {
return users.filter((user) => {
// Check active status
if (criteria.activeOnly && !user.active) {
return false;
}
// Check role
if (criteria.role && user.role !== criteria.role) {
return false;
}
// Check search query
if (criteria.query) {
const query = criteria.query.toLowerCase();
const matchesName = user.name.toLowerCase().includes(query);
const matchesEmail = user.email.toLowerCase().includes(query);
if (!matchesName && !matchesEmail) {
return false;
}
}
return true;
});
}
// Tests
console.log(searchUsers({ activeOnly: true }));
// Alice, Bob, Dave
console.log(searchUsers({ role: 'user' }));
// Bob, Dave
console.log(searchUsers({ query: 'alice', activeOnly: true }));
// Alice
console.log(searchUsers({ role: 'user', activeOnly: true, query: 'dave' }));
// Dave
Exercise 4: Data Pipeline
Create a complete data pipeline that:
- Filters out invalid entries
- Transforms the data
- Groups by category
- Calculates statistics
interface RawData {
id: number;
value: number | null;
category: string;
timestamp: string;
}
const rawData: RawData[] = [
{ id: 1, value: 100, category: 'A', timestamp: '2024-01-01' },
{ id: 2, value: null, category: 'B', timestamp: '2024-01-02' },
{ id: 3, value: 200, category: 'A', timestamp: '2024-01-03' },
{ id: 4, value: 150, category: 'B', timestamp: '2024-01-04' },
{ id: 5, value: 300, category: 'A', timestamp: '2024-01-05' },
{ id: 6, value: null, category: 'A', timestamp: '2024-01-06' },
];
// Create a report with:
// - Only entries with valid values
// - Grouped by category
// - Each category has: count, total, average, min, max
Solution
interface CategoryStats {
count: number;
total: number;
average: number;
min: number;
max: number;
}
// Filter valid entries
const validData = rawData.filter(
(item): item is RawData & { value: number } => item.value !== null
);
// Group and calculate stats
const report = validData.reduce(
(acc, item) => {
if (!acc[item.category]) {
acc[item.category] = {
count: 0,
total: 0,
average: 0,
min: Infinity,
max: -Infinity,
};
}
const stats = acc[item.category];
stats.count++;
stats.total += item.value;
stats.min = Math.min(stats.min, item.value);
stats.max = Math.max(stats.max, item.value);
return acc;
},
{} as Record<string, CategoryStats>
);
// Calculate averages
Object.values(report).forEach((stats) => {
stats.average = stats.total / stats.count;
});
console.log(report);
// {
// A: { count: 3, total: 600, average: 200, min: 100, max: 300 },
// B: { count: 1, total: 150, average: 150, min: 150, max: 150 }
// }
Key Takeaways
- map() transforms each element - use for changing data shape
- filter() selects elements - use for removing unwanted data
- reduce() aggregates to single value - use for sums, grouping, building objects
- Chain methods for complex transformations - filter first, then map
- find() returns first match, some()/every() return booleans
- Array methods return new arrays - they do not mutate the original
- Use type guards with filter for type-safe narrowing
- Consider performance - combine operations when iterating large arrays
Resources
| Resource | Type | Level |
|---|---|---|
| MDN: Array.prototype.map() | Documentation | Beginner |
| MDN: Array.prototype.filter() | Documentation | Beginner |
| MDN: Array.prototype.reduce() | Documentation | Intermediate |
| JavaScript.info: Array methods | Tutorial | Beginner |
Next Lesson
Now that you can transform data, let us learn how to validate that the data is correct using Zod.