From Zero to AI

Lesson 5.2: Data Transformation

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Transform arrays using map() to change data shape
  2. Filter data using filter() with complex conditions
  3. Aggregate data using reduce() for calculations and grouping
  4. Chain multiple transformations together
  5. 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:

  1. Filters out invalid entries
  2. Transforms the data
  3. Groups by category
  4. 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

  1. map() transforms each element - use for changing data shape
  2. filter() selects elements - use for removing unwanted data
  3. reduce() aggregates to single value - use for sums, grouping, building objects
  4. Chain methods for complex transformations - filter first, then map
  5. find() returns first match, some()/every() return booleans
  6. Array methods return new arrays - they do not mutate the original
  7. Use type guards with filter for type-safe narrowing
  8. 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.

Continue to Lesson 5.3: Data Validation with Zod