TypeDrop

2026-10-08 Challenge

2026-10-08 Hard

Typed Schema-Validated Query Builder

You're building the data-access layer for a multi-tenant analytics platform. Consumers declare a typed query against a known table schema — selecting columns, filtering rows, sorting, and paginating — and the compiler must enforce that every referenced column exists on the target table, that filter value types match the column's declared type, and that the final result rows contain exactly the selected columns with their correct types.

Goals

  • Define `InferRow<S>` as a mapped type that converts a TableSchema into a typed row object using `ColumnTypeMap`.
  • Define `AnyFilterClause<S>` as a distributive mapped type that produces a per-column discriminated union where `op` and `value` are co-constrained to each column's declared kind.
  • Type `QueryDescriptor` and `QueryResult` so that selected columns flow through as a const tuple `Cols`, and result rows are typed as `Pick<InferRow<S>, Cols[number]>` — with `Cols` inferred automatically from the `select` field.
  • Implement `createTableHandle` so that `query` correctly filters (AND logic), sorts (multi-column, stable), paginates (`offset`/`limit`), and projects only selected columns — all without `any` or type assertions.
challenge.ts

// Core schema → row inference
type ColumnTypeMap = { string: string; number: number; boolean: boolean; date: Date };
type ColumnKind    = keyof ColumnTypeMap;
type TableSchema   = Record<string, ColumnKind>;

type InferRow<S extends TableSchema> = { [K in keyof S]: ColumnTypeMap[S[K]] };

// Per-column operator constraints
type OpsForKind<K extends ColumnKind> =
  K extends "number"  ? "eq" | "neq" | "gt" | "lt" | "gte" | "lte" :
  K extends "string"  ? "eq" | "neq" | "contains" | "startsWith"   :
  K extends "boolean" ? "eq" | "neq"                                :
  K extends "date"    ? "eq" | "neq" | "gt" | "lt" | "gte" | "lte" :
  never;

// Co-constrained filter clause: op and value are tied to the column's type
type FilterClause<S extends TableSchema, Col extends keyof S> = {
  column: Col;
  op:     OpsForKind<S[Col]>;
  value:  ColumnTypeMap[S[Col]];
};

// Union over all columns — lets where-arrays mix columns safely
type AnyFilterClause<S extends TableSchema> = { [Col in keyof S]: FilterClause<S, Col> }[keyof S];

// Result rows contain ONLY selected columns, correctly typed
type QueryResult<S extends TableSchema, Cols extends ReadonlyArray<keyof S>> = {
  rows: Array<Pick<InferRow<S>, Cols[number]>>;
  totalCount: number;
  executedAt: Date;
};

// Entry point — Cols inferred from select, no manual type args needed
declare function createTableHandle<S extends TableSchema>(
  schema: S,
  data: ReadonlyArray<InferRow<S>>
): TableHandle<S>;
Hints (click to reveal)

Hints

  • For `AnyFilterClause<S>`, build a mapped type `{ [Col in keyof S]: FilterClause<S, Col> }` then index it with `[keyof S]` to distribute into a union — each member of the union carries its own `column`, `op`, and `value` types.
  • To get `Cols` inferred from the `select` field without a manual type argument, declare `query<Cols extends ReadonlyArray<keyof S>>(descriptor: QueryDescriptor<S, Cols>)` — TypeScript will unify `Cols` against the literal tuple passed to `select` when the caller uses `as const`.
  • In `evaluateFilter`, use a `switch` on `filter.op` to narrow the operator; then use `instanceof Date` checks before numeric comparisons to satisfy the type checker for date ops — no casts needed if you guard correctly.

Or clone locally

git clone -b challenge/2026-10-08 https://github.com/niltonheck/typedrop.git