Hone logo
Hone
Problems

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. Multiple where calls should be allowed and combined with logical AND.
  • orderBy(field: string, direction: 'ASC' | 'DESC'): Specifies sorting criteria. Multiple orderBy calls 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:

  1. 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).
  2. Chaining: Methods like select, where, orderBy, and limit should return this or a type that allows further chaining.
  3. execute() Behavior: The execute() method should only be callable after a select clause has been defined. It should return a string representing the constructed query.
  4. Immutability (Optional but Recommended): Ideally, each method call should return a new instance of the QueryBuilder with 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 select or limit more than once.
  • Calling execute() before calling select().
  • Calling execute() without any prior methods.
  • Chaining where and orderBy multiple 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 QueryBuilder class 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!

Loading editor...
typescript