Lesson 2
Managing User Data and Aggregation with TypeScript
Introduction

Welcome to today's lesson on applying data filtering and aggregation in a real-world scenario using a user management system. We'll start by building a foundational structure that can handle basic user operations. Then, we'll expand it by introducing more advanced functionalities that allow filtering and aggregating user data.

Starter Task Methods

In our starter task, we will implement a class that manages basic operations on a collection of user data, specifically handling adding new users, retrieving user profiles, and updating user profiles.

Here are the starter task methods with TypeScript type annotations:

  • addUser(userId: string, age: number, country: string, subscribed: boolean): boolean - adds a new user with the specified attributes. Returns true if the user was added successfully and false if a user with the same userId already exists.
  • getUser(userId: string): UserProfile | null - returns the user's profile as an object if the user exists; otherwise, returns null.
  • updateUser(userId: string, age: number | null, country: string | null, subscribed: boolean | null): boolean - updates the user's profile based on non-null parameters. Returns true if the user exists and was updated, false otherwise.

The UserProfile data type is an interface that defines the structure of a user's profile, consisting of three properties: age which is a number, country which is a string, and subscribed which is a boolean. This interface ensures that every user profile adheres to this defined structure.

Solution for the Starter Task

The TypeScript implementation of our starter task is shown below:

TypeScript
1type UserProfile = { 2 age: number; 3 country: string; 4 subscribed: boolean; 5}; 6 7class UserManager { 8 private users: Map<string, UserProfile> = new Map(); // Define user container as a Map 9 10 // Method to add a new user 11 addUser(userId: string, age: number, country: string, subscribed: boolean): boolean { 12 if (this.users.has(userId)) { 13 return false; // Return false if userId already exists 14 } 15 this.users.set(userId, { age, country, subscribed }); // Add user to the Map 16 return true; // Return true to indicate successful addition 17 } 18 19 // Method to retrieve a user's profile 20 getUser(userId: string): UserProfile | null { 21 return this.users.get(userId) || null; // Return the user profile or null if the user does not exist 22 } 23 24 // Method to update a user's profile 25 updateUser(userId: string, age: number | null, country: string | null, subscribed: boolean | null): boolean { 26 if (!this.users.has(userId)) { 27 return false; // Return false if the user does not exist 28 } 29 const profile = this.users.get(userId)!; // Retrieve existing profile (non-null assertion for Map get) 30 if (age !== null) { 31 profile.age = age; // Update age if provided 32 } 33 if (country !== null) { 34 profile.country = country; // Update country if provided 35 } 36 if (subscribed !== null) { 37 profile.subscribed = subscribed; // Update subscription status if provided 38 } 39 this.users.set(userId, profile); // Update the Map with the modified profile 40 return true; // Return true to indicate successful update 41 } 42} 43 44// Example usage 45const um = new UserManager(); 46console.log(um.addUser("u1", 25, "USA", true)); // true 47console.log(um.addUser("u2", 30, "Canada", false)); // true 48console.log(um.addUser("u1", 22, "Mexico", true)); // false 49console.log(um.getUser("u1")); // { age: 25, country: "USA", subscribed: true } 50console.log(um.updateUser("u1", 26, null, null)); // true 51console.log(um.updateUser("u3", 19, "UK", false)); // false

This implementation covers all our starter methods. Let's move forward and introduce more complex functionalities.

Introducing New Methods for Data Filtering and Aggregation

With our foundational structure in place, it's time to add functionalities for filtering user data and aggregating statistics.

Here are the new methods to implement with TypeScript type annotations:

  • filterUsers(minAge: number | null, maxAge: number | null, country: string | null, subscribed: boolean | null): string[]:
    • Returns the list of user IDs that match the specified criteria. Criteria can be null, meaning that the criterion should not be applied during filtering.
  • aggregateStats(): { totalUsers: number; averageAge: number; subscribedRatio: number } - returns statistics in the form of an object:
    • totalUsers: Total number of users
    • averageAge: Average age of all users (rounded down to the nearest integer)
    • subscribedRatio: Ratio of subscribed users to total users (as a float with two decimals)
Step 1: Adding 'filterUsers' Method

This method filters users based on the criteria provided. Let's see how it works in TypeScript:

TypeScript
1class UserManager { 2 // Existing methods... 3 4 // Method to filter users based on criteria 5 filterUsers(minAge: number | null, maxAge: number | null, country: string | null, subscribed: boolean | null): string[] { 6 const filteredUsers: string[] = []; 7 for (const [userId, profile] of this.users.entries()) { 8 // Check minimum age criterion 9 if (minAge !== null && profile.age < minAge) { 10 continue; 11 } 12 // Check maximum age criterion 13 if (maxAge !== null && profile.age > maxAge) { 14 continue; 15 } 16 // Check country criterion 17 if (country !== null && profile.country !== country) { 18 continue; 19 } 20 // Check subscription status criterion 21 if (subscribed !== null && profile.subscribed !== subscribed) { 22 continue; 23 } 24 // Add userId to filteredUsers if all criteria are met 25 filteredUsers.push(userId); 26 } 27 return filteredUsers; // Return the list of filtered user IDs 28 } 29} 30 31// Example usage of the new method 32const um = new UserManager(); 33um.addUser("u1", 25, "USA", true); 34um.addUser("u2", 30, "Canada", false); 35um.addUser("u3", 22, "USA", true); 36console.log(um.filterUsers(20, 30, "USA", true)); // ["u1", "u3"] 37console.log(um.filterUsers(null, 28, null, null)); // ["u1", "u3"] 38console.log(um.filterUsers(null, null, "Canada", false)); // ["u2"]
  • The filterUsers method filters users based on minAge, maxAge, country, and subscribed status criteria.
  • It iterates over the users object and checks each user's profile against the provided criteria.
  • Users who meet all the criteria are added to the filteredUsers list, which is then returned.
  • The example usage demonstrates the addition of users and how to filter them based on different criteria.
Step 2: Adding 'aggregateStats' Method

This method aggregates statistics from the user profiles. Let's implement it in TypeScript:

TypeScript
1type UserStatistics = { 2 totalUsers: number; 3 averageAge: number; 4 subscribedRatio: number; 5}; 6 7class UserManager { 8 // Existing methods... 9 10 // Method to aggregate statistics from user profiles 11 aggregateStats(): UserStatistics { 12 const totalUsers = this.users.size; // Get the total number of users 13 if (totalUsers === 0) { // If no users, return zeroed statistics 14 return { totalUsers: 0, averageAge: 0, subscribedRatio: 0.00 }; 15 } 16 17 // Calculate total age by summing ages of all users 18 let totalAge = 0; 19 let subscribedUsers = 0; 20 for (const profile of this.users.values()) { 21 totalAge += profile.age; 22 if (profile.subscribed) { 23 subscribedUsers++; 24 } 25 } 26 27 // Calculate average age (rounded down) 28 const averageAge = Math.floor(totalAge / totalUsers); 29 // Calculate subscribed ratio (to two decimals) 30 const subscribedRatio = parseFloat((subscribedUsers / totalUsers).toFixed(2)); 31 32 return { totalUsers, averageAge, subscribedRatio }; // Return statistics object 33 } 34} 35 36// Using `um` from the previous section 37console.log(um.aggregateStats()); // { totalUsers: 3, averageAge: 25, subscribedRatio: 0.67 }
  • The aggregateStats method calculates aggregate statistics about users and returns them as an object.
  • It begins by determining totalUsers, the total number of users.
  • If no users exist, it returns an object with all statistics set to zero.
  • With users present, it calculates totalAge by summing up all users' ages and counts how many are subscribedUsers.
  • Next, it computes averageAge by dividing totalAge by totalUsers and rounding down to the nearest integer.
  • It also calculates subscribedRatio by dividing subscribedUsers by totalUsers, rounding the result to two decimal places.
  • The resulting object includes totalUsers, averageAge, and subscribedRatio.
The Final Solution

Here's the complete UserManager class with all methods, including the new ones for filtering and aggregation, all implemented in TypeScript:

TypeScript
1type UserProfile = { 2 age: number; 3 country: string; 4 subscribed: boolean; 5}; 6 7type UserStatistics = { 8 totalUsers: number; 9 averageAge: number; 10 subscribedRatio: number; 11}; 12 13class UserManager { 14 private users: Map<string, UserProfile> = new Map(); // Define user container as a Map 15 16 // Method to add a new user 17 addUser(userId: string, age: number, country: string, subscribed: boolean): boolean { 18 if (this.users.has(userId)) { 19 return false; // Return false if userId already exists 20 } 21 this.users.set(userId, { age, country, subscribed }); // Add user to the Map 22 return true; // Return true to indicate successful addition 23 } 24 25 // Method to retrieve a user's profile 26 getUser(userId: string): UserProfile | null { 27 return this.users.get(userId) || null; // Return the user profile or null if the user does not exist 28 } 29 30 // Method to update a user's profile 31 updateUser(userId: string, age: number | null, country: string | null, subscribed: boolean | null): boolean { 32 if (!this.users.has(userId)) { 33 return false; // Return false if the user does not exist 34 } 35 const profile = this.users.get(userId)!; // Retrieve existing profile (non-null assertion for Map get) 36 if (age !== null) { 37 profile.age = age; // Update age if provided 38 } 39 if (country !== null) { 40 profile.country = country; // Update country if provided 41 } 42 if (subscribed !== null) { 43 profile.subscribed = subscribed; // Update subscription status if provided 44 } 45 this.users.set(userId, profile); // Update the Map with the modified profile 46 return true; // Return true to indicate successful update 47 } 48 filterUsers(minAge: number | null, maxAge: number | null, country: string | null, subscribed: boolean | null): string[] { 49 const filteredUsers: string[] = []; 50 for (const [userId, profile] of this.users.entries()) { 51 // Check minimum age criterion 52 if (minAge !== null && profile.age < minAge) { 53 continue; 54 } 55 // Check maximum age criterion 56 if (maxAge !== null && profile.age > maxAge) { 57 continue; 58 } 59 // Check country criterion 60 if (country !== null && profile.country !== country) { 61 continue; 62 } 63 // Check subscription status criterion 64 if (subscribed !== null && profile.subscribed !== subscribed) { 65 continue; 66 } 67 // Add userId to filteredUsers if all criteria are met 68 filteredUsers.push(userId); 69 } 70 return filteredUsers; // Return the list of filtered user IDs 71 } 72 aggregateStats(): UserStatistics { 73 const totalUsers = this.users.size; // Get the total number of users 74 if (totalUsers === 0) { // If no users, return zeroed statistics 75 return { totalUsers: 0, averageAge: 0, subscribedRatio: 0.00 }; 76 } 77 78 // Calculate total age by summing ages of all users 79 let totalAge = 0; 80 let subscribedUsers = 0; 81 for (const profile of this.users.values()) { 82 totalAge += profile.age; 83 if (profile.subscribed) { 84 subscribedUsers++; 85 } 86 } 87 88 // Calculate average age (rounded down) 89 const averageAge = Math.floor(totalAge / totalUsers); 90 // Calculate subscribed ratio (to two decimals) 91 const subscribedRatio = parseFloat((subscribedUsers / totalUsers).toFixed(2)); 92 93 return { totalUsers, averageAge, subscribedRatio }; // Return statistics object 94 } 95} 96 97// Example usage 98const um = new UserManager(); 99um.addUser("u1", 25, "USA", true); 100um.addUser("u2", 30, "Canada", false); 101um.addUser("u3", 22, "USA", true); 102 103console.log(um.filterUsers(20, 30, "USA", true)); // ["u1", "u3"] 104console.log(um.filterUsers(null, 28, null, null)); // ["u1", "u3"] 105console.log(um.filterUsers(null, null, "Canada", false)); // ["u2"] 106 107console.log(um.aggregateStats()); // { totalUsers: 3, averageAge: 25, subscribedRatio: 0.67 }
Lesson Summary

Great job! Today, you've learned how to effectively handle user data in TypeScript by implementing advanced functionalities like filtering and aggregation on top of a basic system. TypeScript's robust typing aids significantly in data management and helps catch potential errors during development. This is a critical skill in real-life software development, where you often need to extend existing systems to meet new requirements.

I encourage you to practice solving similar challenges to solidify your understanding of data filtering and aggregation with strong type support. Happy coding, and see you in the next lesson!

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