Advanced Generic Constraints: Building a Type-Safe Data Processor
This challenge focuses on creating a flexible and robust data processing function in TypeScript using advanced generic constraints. You will build a processData function that can handle various data structures (arrays, objects) and apply transformations based on specific conditions, ensuring type safety throughout the process. This is useful for creating reusable utility functions that adapt to different data types without sacrificing compile-time checks.
Problem Description
Your task is to implement a TypeScript function named processData. This function should accept two arguments:
data: The data to be processed. This can be an array of items or an object where keys are strings and values are of a consistent type.processor: A function that takes a single item from thedataand returns a processed version of that item.
The processData function should dynamically determine the type of data (array or object) and apply the processor function accordingly.
Key Requirements:
- Generics: The function must be generic to handle various input data types and their corresponding processed output types.
- Conditional Type for Data Structure: Implement logic to differentiate between array and object inputs for
data. - Type-Safe Processing: Ensure that the
processorfunction is applied correctly to elements of an array or values of an object. - Return Type: The
processDatafunction should return a new data structure of the same shape as the inputdatabut with the processed values.
Expected Behavior:
- If
datais an array,processDatashould return a new array where each element is the result of applyingprocessorto the corresponding element of the input array. - If
datais an object,processDatashould return a new object with the same keys, where each value is the result of applyingprocessorto the corresponding value of the input object.
Edge Cases to Consider:
- Empty arrays and empty objects.
Examples
Example 1: Processing an array of numbers.
const numbers = [1, 2, 3, 4];
const double = (n: number): number => n * 2;
const processedNumbers = processData(numbers, double);
// processedNumbers should be [2, 4, 6, 8]
// The type of processedNumbers should be number[]
Example 2: Processing an object of strings.
const user = {
firstName: "John",
lastName: "Doe",
};
const capitalize = (s: string): string => s.toUpperCase();
const processedUser = processData(user, capitalize);
// processedUser should be { firstName: "JOHN", lastName: "DOE" }
// The type of processedUser should be { firstName: string; lastName: string; }
Example 3: Processing an array of objects.
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Laptop", price: 1200 },
{ id: 2, name: "Mouse", price: 25 },
];
const applyDiscount = (p: Product): Product => ({
...p,
price: p.price * 0.9, // 10% discount
});
const discountedProducts = processData(products, applyDiscount);
// discountedProducts should be an array of Product objects with discounted prices.
// The type of discountedProducts should be Product[]
Constraints
- The
datainput will either be an array or a plain JavaScript object with string keys. - The
processorfunction will always accept a single argument and return a single value. - The function should handle types correctly, ensuring that the output type reflects the transformation applied by the
processor.
Notes
Consider using TypeScript's conditional types and mapped types to create a sophisticated and type-safe processData function. Think about how you can infer the types of the input data and the output of the processor to construct the correct return type. You might want to define type helpers to discriminate between array and object types.