Lesson 3
Mastering Data Aggregation and JSON Streams with TypeScript
Introduction

Welcome to our lesson on mastering data aggregation and data streams with JSON formatting in TypeScript. In this lesson, we'll start by building a basic sales records aggregator. Leveraging TypeScript's type safety, the lesson will empower you with strong typing benefits, ensuring your data handling is robust and error-free. By the end of this session, you'll be able to manage and format data streams efficiently using TypeScript.

Starter Task Methods and Their Definitions

To begin, we'll implement a basic sales record aggregator. Here are the methods we'll be focusing on:

  • addSale(saleId: string, amount: number): void - Adds a sale record with a unique identifier saleId and an amount. If a sale with the same saleId already exists, it updates the amount.

  • getSale(saleId: string): number | undefined - Retrieves the sale amount associated with the saleId. If the sale does not exist, it returns undefined.

  • deleteSale(saleId: string): boolean - Deletes the sale record with the given saleId. Returns true if the sale was deleted and false if the sale does not exist.

Are these methods clear so far? Great! Let's now look at how we would implement them in TypeScript.

Starter Task Solution

Here is the complete code for the starter task:

TypeScript
1class SalesAggregator { 2 private sales: Record<string, number>; 3 4 constructor() { 5 this.sales = {}; 6 } 7 8 addSale(saleId: string, amount: number): void { 9 this.sales[saleId] = amount; 10 } 11 12 getSale(saleId: string): number | undefined { 13 return this.sales[saleId]; 14 } 15 16 deleteSale(saleId: string): boolean { 17 if (saleId in this.sales) { 18 delete this.sales[saleId]; 19 return true; 20 } 21 return false; 22 } 23} 24 25// Example Usage 26const aggregator = new SalesAggregator(); 27 28// Add sales 29aggregator.addSale('001', 100.50); 30aggregator.addSale('002', 200.75); 31 32// Get sale 33console.log(aggregator.getSale('001')); // Output: 100.5 34 35// Delete sale 36console.log(aggregator.deleteSale('002')); // Output: true 37console.log(aggregator.getSale('002')); // Output: undefined

Explanation:

  • The constructor method initializes a private object sales to store records.
  • The addSale method adds a new sale or updates the amount for an existing sale ID.
  • The getSale method retrieves the amount for a given sale ID or returns undefined if it does not exist.
  • The deleteSale method removes the sale record for the given sale ID or returns false if the sale does not exist.

With the basic aggregator implemented, let's extend it with more advanced functionalities.

New Methods and Their Definitions

To increase the complexity and usefulness of our sales aggregator, we'll adjust some existing methods and introduce some new methods and functionalities involving JSON.

Let's define custom types to simplify the method return types and the sales attribute:

TypeScript
1type SaleRecord = { amount: number, date: string }; 2type AggregateResult = { totalSales: number, totalAmount: number }; 3type Sale = { saleId: string, amount: number, date: string };

Here's the updated class with type definitions:

  • addSale(saleId: string, amount: number, date: string): void - Adds or updates a sale record with saleId, amount, and a date in the format "YYYY-MM-DD".

  • aggregateSales(minAmount: number = 0): AggregateResult - Returns an AggregateResult object with the total number of sales and the total amount of sales above minAmount.

  • formatSales(minAmount: number = 0): string - Returns sales data filtered by minAmount in JSON format, including aggregate statistics.

  • getSalesInDateRange(startDate: string, endDate: string): Sale[] - Retrieves sales within the specified date range.

Let's implement these methods step-by-step.

Step 1: Enhancing the 'addSale' Method to Include Date

We'll first modify the addSale method to accept a date.

TypeScript
1type SaleRecord = { amount: number, date: string }; 2 3class SalesAggregator { 4 private sales: Record<string, SaleRecord>; 5 6 constructor() { 7 this.sales = {}; 8 } 9 10 addSale(saleId: string, amount: number, date: string): void { 11 this.sales[saleId] = { amount, date }; 12 } 13 14 // Previous method implementations (getSale, deleteSale) 15}

This ensures each sale record includes a date in addition to the amount, maintaining type consistency.

Step 2: Implementing the 'aggregateSales' Method

Now, let's create the aggregateSales method:

TypeScript
1type AggregateResult = { totalSales: number, totalAmount: number }; 2 3class SalesAggregator { 4 5 // Previous implementations ... 6 7 aggregateSales(minAmount: number = 0): AggregateResult { 8 let totalSales = 0; 9 let totalAmount = 0.0; 10 for (const sale of Object.values(this.sales)) { 11 if (sale.amount > minAmount) { 12 totalSales += 1; 13 totalAmount += sale.amount; 14 } 15 } 16 return { totalSales, totalAmount }; 17 } 18} 19 20// Example instantiation and usage 21const aggregator = new SalesAggregator(); 22aggregator.addSale('001', 100.50, '2023-01-01'); 23aggregator.addSale('002', 200.75, '2023-01-15'); 24 25// Aggregate sales 26console.log(aggregator.aggregateSales(50)); 27// Output: { totalSales: 2, totalAmount: 301.25 }

This method iterates through the sales and sums those that exceed the minAmount, leveraging TypeScript's type annotations for safe computations.

Step 3: Implementing the 'formatSales' Method

The formatSales method is designed to output sales data as JSON, filtered by a specified minimum amount (minAmount) and including aggregated sales statistics. Below is an explanation of the key components of this method:

TypeScript
1class SalesAggregator { 2 3 // Previous implementations ... 4 5 formatSales(minAmount: number = 0): string { 6 // Filter and format sales data based on minAmount. 7 const filteredSales = Object.entries(this.sales) 8 .filter(([_, sale]) => sale.amount > minAmount) 9 .map(([saleId, sale]) => ({ saleId, amount: sale.amount, date: sale.date })); 10 11 // Aggregate sales statistics. 12 const statistics = this.aggregateSales(minAmount); 13 14 // Combine sales data and statistics into the final result. 15 const result = { 16 sales: filteredSales, 17 statistics 18 }; 19 return JSON.stringify(result); // Convert the result to JSON. 20 } 21} 22// Example instantiation and usage from previous implementation 23 24// Format sales to JSON 25console.log(aggregator.formatSales(50)); 26// Output: '{"sales":[{"saleId":"001","amount":100.5,"date":"2023-01-01"},{"saleId":"002","amount":200.75,"date":"2023-01-15"}],"statistics":{"totalSales":2,"totalAmount":301.25}}'

Method Chaining:

  1. Object.entries(this.sales):

    • This converts the sales object into an array of key-value pairs. Each pair is an array where the first element is the saleId and the second is a SaleRecord object containing the amount and date.
  2. filter(([_, sale]) => sale.amount > minAmount):

    • This filters the entries based on the condition that the sale's amount must be greater than the minAmount. The use of _ as a placeholder for saleId implies it's not utilized in this operation.
  3. map(([saleId, sale]) => ({ saleId, amount: sale.amount, date: sale.date })):

    • This maps each filtered entry to a new object containing saleId, amount, and date. The method takes each entry (a tuple of saleId and SaleRecord) and creates a new object for simplified representation.

Method chaining is a succinct approach that allows several array operations (filter and map) to be linked consecutively, transforming the data step-by-step efficiently.

Role of JSON.stringify():

  • JSON.stringify(result); is used to convert the final JavaScript object (result) into a JSON string. This is crucial for data interchange formats or APIs needing serialized data. JSON strings are language-independent and provide a standardized format, making them ideal for storing or transmitting data across different systems or components.

This method efficiently combines filtering, mapping, and aggregating operations to produce a JSON representation of the sales data, clearly illustrating both data attributes and derived statistics.

Step 4: Implementing the 'getSalesInDateRange' Method

The getSalesInDateRange method is developed to retrieve sales records that fall within a specified date range.

TypeScript
1type Sale = { saleId: string, amount: number, date: string }; 2 3class SalesAggregator { 4 5 // Previous implementations ... 6 7 getSalesInDateRange(startDate: string, endDate: string): Sale[] { 8 const start = new Date(startDate); 9 const end = new Date(endDate); 10 return Object.entries(this.sales) 11 .filter(([_, sale]) => { 12 const saleDate = new Date(sale.date); 13 return saleDate >= start && saleDate <= end; // Check if the sale is within the date range. 14 }) 15 .map(([saleId, sale]) => ({ saleId, ...sale })); 16 } 17} 18// Example instantiation and usage from previous implementation 19 20// Get sales in date range 21console.log(aggregator.getSalesInDateRange('2023-01-01', '2023-12-31')); 22// Output: [{ saleId: '001', amount: 100.5, date: '2023-01-01' }, { saleId: '002', amount: 200.75, date: '2023-01-15' }]

Method Chaining:

  1. Object.entries(this.sales):

    • Converts the sales object into an array of key-value pairs. Each entry is an array [saleId, SaleRecord]. This allows the function to easily operate on each sale record.
  2. filter(([_, sale]) => { ... }):

    • Filters entries to include only those whose saleDate falls within the specified start and end dates.
    • The lambda function:
      • Converts the sale.date string into a Date object (saleDate).
      • Performs a comparison to ensure saleDate is greater than or equal to start and less than or equal to end.
  3. map(([saleId, sale]) => ({ saleId, ...sale })):

    • Maps each filtered entry into a structured Sale object containing saleId, amount, and date.
    • The spread operator ...sale is used to copy properties from sale (amount, date) into the new object.

Method chaining in this method provides a streamlined and efficient way to process sales data: filtering checks dates, while mapping creates structured objects ready for use. This approach simplifies data transformations by reducing boilerplate and enhancing readability, wherein multiple operations are seamlessly linked together to produce the desired result set.

Lesson Summary

Congratulations! You've now extended a basic sales aggregator to an advanced one capable of filtering, aggregating, and formatting data in JSON. TypeScript's strong typing offers significant advantages, such as type safety and prevention of runtime errors, especially when dealing with large datasets. Feel free to experiment further with TypeScript features to reinforce your understanding. Now, let's move on to the practice section to apply what you've learned!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.