Implementing Fluent API Types in TypeScript
This challenge focuses on designing and implementing type-safe fluent APIs in TypeScript. Fluent APIs, often called "method chaining," allow for a more readable and expressive way to construct objects or perform operations by chaining method calls. The goal is to leverage TypeScript's advanced type system to ensure that these chained calls are not only syntactically correct but also semantically sound.
Problem Description
You are tasked with creating a TypeScript class that represents a "QueryBuilder." This QueryBuilder should allow users to construct database-like queries in a fluent manner. The key requirement is that the type system should guide the user, preventing invalid query constructions at compile time.
Specifically, the QueryBuilder class should support the following operations:
select(fields: string[]): Specifies the fields to retrieve. This method should only be callable once.where(condition: string): Adds a filtering condition. Multiplewherecalls should be allowed and combined with logical AND.orderBy(field: string, direction: 'ASC' | 'DESC'): Specifies sorting criteria. MultipleorderBycalls should be allowed and applied sequentially.limit(count: number): Sets a limit on the number of results. This method should only be callable once.execute(): Finalizes the query and returns a string representation of the query.
Key Requirements:
- Type Safety for Method Calls: The type system should prevent calling methods in an invalid order or calling methods multiple times if they are intended to be called only once (e.g.,
select,limit). - Chaining: Methods like
select,where,orderBy, andlimitshould returnthisor a type that allows further chaining. execute()Behavior: Theexecute()method should only be callable after aselectclause has been defined. It should return a string representing the constructed query.- Immutability (Optional but Recommended): Ideally, each method call should return a new instance of the
QueryBuilderwith the updated state, rather than modifying the existing instance. This promotes a more functional programming style and predictable behavior.
Expected Behavior:
A valid query construction would look like:
const query = new QueryBuilder()
.select(['id', 'name'])
.where('status = "active"')
.orderBy('id', 'DESC')
.limit(10)
.execute();
// query would be a string like: "SELECT id, name WHERE status = "active" ORDER BY id DESC LIMIT 10"
Edge Cases to Consider:
- Calling
selectorlimitmore than once. - Calling
execute()before callingselect(). - Calling
execute()without any prior methods. - Chaining
whereandorderBymultiple times.
Examples
Example 1: Basic Valid Query
Input:
const queryBuilder = new QueryBuilder();
const queryString = queryBuilder
.select(['username', 'email'])
.where('age > 18')
.orderBy('username', 'ASC')
.limit(50)
.execute();
Output:
queryString will be the string: "SELECT username, email WHERE age > 18 ORDER BY username ASC LIMIT 50"
Example 2: Invalid Order of Operations (compile-time error)
Input:
const queryBuilder = new QueryBuilder();
const queryString = queryBuilder
.where('is_admin = true') // Calling 'where' before 'select'
.select(['user_id'])
.execute();
Output:
This code should produce a TypeScript compilation error because 'select' was not called before 'execute', and potentially even an error if 'where' is called before 'select' depending on the strictness of your type definition.
Example 3: Invalid Repeated Method Call (compile-time error)
Input:
const queryBuilder = new QueryBuilder();
const queryString = queryBuilder
.select(['id'])
.select(['name']) // Calling 'select' twice
.execute();
Output:
This code should produce a TypeScript compilation error because 'select' is called more than once.
Example 4: Minimum Valid Query
Input:
const queryBuilder = new QueryBuilder();
const queryString = queryBuilder
.select(['*'])
.execute();
Output:
queryString will be the string: "SELECT *"
Constraints
- The solution must be written entirely in TypeScript.
- The primary goal is to achieve compile-time type safety. Runtime errors for invalid query constructions should be avoided by leveraging the type system.
- The
QueryBuilderclass should be designed to be extensible, though this is not a strict requirement for this challenge. - Performance is not a primary concern; clarity and type safety are paramount.
Notes
Consider using conditional types, discriminated unions, or specific type states to enforce the order and frequency of method calls. Think about how to represent the "state" of the query builder in its type. For instance, a query builder that hasn't called select yet should have a different type than one that has.
This challenge requires a deep understanding of TypeScript's advanced type manipulation features. Good luck!