Building a Row Polymorphism System in TypeScript
This challenge focuses on creating a flexible system in TypeScript that allows you to work with "rows" of data in a polymorphic way. This means you should be able to define functions and types that operate on data structures, treating them as rows with potentially different sets of columns, without losing type safety. This is particularly useful when dealing with databases, APIs, or any system where data structures can vary but share a common "row" concept.
Problem Description
Your task is to design and implement a system in TypeScript that enables row polymorphism. This system should allow you to:
- Define Row Types: Create generic types that represent a "row" of data. A row is essentially an object with named properties (columns).
- Extract Specific Columns: Write a function that can extract a subset of columns from a given row, returning a new row object containing only the specified columns.
- Transform Rows: Develop functions that can transform a row into another row type, potentially changing the names or types of columns, while ensuring type safety.
- Handle Missing Columns Gracefully: Your system should be robust enough to handle cases where a requested column might not exist in the input row, without causing runtime errors or type mismatches.
Key Requirements:
- Type Safety: All operations must be type-safe. TypeScript should enforce that you are only accessing existing columns and that transformations maintain valid type relationships.
- Generics: Leverage TypeScript generics to make your system reusable with any row structure.
- Clear Abstraction: The system should provide a clear and intuitive way to define and manipulate rows.
Expected Behavior:
- You should be able to define a base row type and then create new types that are subsets or transformations of that base type.
- Functions for extracting columns should return a new object with only the specified keys and their corresponding values.
- Transformation functions should allow for mapping between different row structures.
Edge Cases:
- Extracting columns that do not exist in the source row.
- Transforming a row where a required source column is missing.
Examples
Example 1: Extracting Columns
// Assume you have a Row type defined in your system
type UserProfile = {
id: number;
name: string;
email: string;
age: number;
};
// Input row
const user: UserProfile = {
id: 1,
name: "Alice",
email: "alice@example.com",
age: 30,
};
// Function to extract specific columns (you'll implement this)
// extractColumns<Row, Keys extends keyof Row>(row: Row, keys: Keys[]): Pick<Row, Keys>
// ...
// Expected output after calling extractColumns(user, ['name', 'email'])
const nameAndEmail = {
name: "Alice",
email: "alice@example.com",
};
Explanation: The extractColumns function should take the user object and an array of keys ('name', 'email') and return a new object containing only those properties. The type of the returned object should be Pick<UserProfile, 'name' | 'email'>.
Example 2: Transforming Rows
// Assume you have a Row type defined in your system
type Product = {
productId: string;
productName: string;
price: number;
};
type SimpleProduct = {
name: string;
cost: number;
};
// Input row
const product: Product = {
productId: "P123",
productName: "Laptop",
price: 1200,
};
// Function to transform rows (you'll implement this)
// transformRow<SourceRow, TargetRow>(row: SourceRow, transformer: (source: SourceRow) => TargetRow): TargetRow
// ...
// Or a more specific transformation function signature:
// transformRow<TSource, TTarget>(source: TSource, mapper: (source: TSource) => TTarget): TTarget
// ...
// Expected output after calling a transformation function:
// transformRow(product, (p) => ({ name: p.productName, cost: p.price }))
const simpleProduct: SimpleProduct = {
name: "Laptop",
cost: 1200,
};
Explanation: A transformation function should take the product object and a mapping function. The mapping function defines how to convert a Product into a SimpleProduct. The overall transformation function should return a SimpleProduct object.
Example 3: Handling Missing Columns During Extraction
type Employee = {
id: number;
firstName: string;
lastName: string;
};
const employee: Employee = {
id: 2,
firstName: "Bob",
lastName: "Smith",
};
// If extractColumns is designed to handle missing keys gracefully (e.g., return undefined for missing keys or omit them)
// Calling extractColumns(employee, ['firstName', 'department'])
// The 'department' key is not present in the 'employee' object.
// Possible Expected Output 1 (omitting missing keys):
const extractedInfo1 = {
firstName: "Bob",
};
// Possible Expected Output 2 (including missing keys as undefined, though this might be less desirable for a strict 'Pick' behavior)
// This would require a different signature and return type. For this challenge, focus on the 'Pick' behavior as in Example 1,
// where TS prevents asking for non-existent keys at compile time. If you want to handle runtime missing keys,
// the extraction function needs to be designed differently. For this challenge, we'll focus on compile-time safety.
// For the purpose of this challenge, assume the Keys parameter is restricted to actual keys of the Row type.
// However, if we were to relax this, a function that returns optional properties might look like:
// type MaybePartial<T, K extends string | number | symbol> = { [P in K]: P extends keyof T ? T[P] : undefined };
// extractColumnsWithUndefined<Row, Keys extends string>(row: Row, keys: Keys[]): MaybePartial<Row, Keys>
// Then the output could be:
// const extractedInfo2 = {
// firstName: "Bob",
// department: undefined
// };
// For this challenge, let's stick to the stricter 'Pick' behavior from Example 1,
// and assume the input `keys` array will only contain valid keys of the `Row` type,
// enforced by TypeScript's type system.
Constraints
- The implementation must be entirely in TypeScript.
- The system should leverage TypeScript's type system (generics, conditional types, mapped types) to ensure type safety.
- Avoid runtime checks for key existence if a compile-time check is possible through the type system. For column extraction, the
Keysparameter should be a union of valid keys of the inputRowtype. - The focus is on the type-level API and its correctness. Performance of the generated JavaScript is a secondary concern, but avoid grossly inefficient patterns.
Notes
- Consider how you will represent a "row" using TypeScript's type system. An object literal is the most natural fit.
- Think about the utility types available in TypeScript (e.g.,
Pick,Omit,Partial,Record) and how they might be used. - For column extraction, you'll likely want to use a generic function that takes a
Rowtype and aKeystype parameter, whereKeysis constrained to be a subset ofkeyof Row. - For row transformation, you might design a generic function that takes the source row type and the target row type, or a function that accepts a mapping function whose signature is inferable.
- The goal is to create a robust and type-safe abstraction for working with varying data structures as if they were structured rows.