TypeScript: type-safe select, where, and orderBy
Write small query helpers whose column names are checked by the compiler with keyof.
On this page
TypeScript generics, keyof, and arrays.
- Write small query helpers whose column names are checked by the compiler with keyof.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A relational query filters rows, sorts them, and chooses columns. In TypeScript a table can be an array of objects, and the type of the row describes its columns. The generic constraint K extends keyof T says that a column name must be one of the row type's property names, so a misspelled column is a compile-time error instead of an empty result at run time.
Pick<T, K> is the type of an object with only the chosen columns, so select returns rows with exactly those properties. The helpers are the in-memory equivalent of WHERE, ORDER BY, and the column list of SELECT. Types are erased when the program runs, so data coming from a real database or an API still needs runtime validation.
Code example
type Row = { id: number; name: string; dept: string; salary: number };
const table: Row[] = [
{ id: 1, name: "Ada", dept: "Eng", salary: 120 },
{ id: 2, name: "Bo", dept: "Ops", salary: 90 },
{ id: 3, name: "Cy", dept: "Eng", salary: 100 },
{ id: 4, name: "Di", dept: "Ops", salary: 80 },
];
function where<T>(rows: T[], predicate: (row: T) => boolean): T[] {
return rows.filter(predicate);
}
function orderBy<T, K extends keyof T>(rows: T[], key: K): T[] {
return [...rows].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0));
}
function select<T, K extends keyof T>(rows: T[], ...keys: K[]): Pick<T, K>[] {
return rows.map((row) => {
const picked = {} as Pick<T, K>;
for (const key of keys) {
picked[key] = row[key];
}
return picked;
});
}
const wellPaid = where(table, (row) => row.salary >= 100);
const sorted = orderBy(wellPaid, "name");
console.log(JSON.stringify(select(sorted, "name", "salary")));
console.log(JSON.stringify(select(orderBy(table, "salary"), "name")));[{"name":"Ada","salary":120},{"name":"Cy","salary":100}]
[{"name":"Di"},{"name":"Bo"},{"name":"Cy"},{"name":"Ada"}]Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
type Row = { id: number; name: string; dept: string; salary: number }; const table: Row[] = [ { id: 1, name: "Ada", dept: "Eng", salary: 120 }, { id: 2, name: "Bo", dept: "Ops", salary: 90 }, { id: 3, name: "Cy", dept: "Eng", salary: 100 }, { id: 4, name: "Di", dept: "Ops", salary: 80 },]; function where<T>(rows: T[], predicate: (row: T) => boolean): T[] { return rows.filter(predicate);} function orderBy<T, K extends keyof T>(rows: T[], key: K): T[] { return [...rows].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0));} function select<T, K extends keyof T>(rows: T[], ...keys: K[]): Pick<T, K>[] { return rows.map((row) => { const picked = {} as Pick<T, K>; for (const key of keys) { picked[key] = row[key]; } return picked; });} const wellPaid = where(table, (row) => row.salary >= 100);const sorted = orderBy(wellPaid, "name");console.log(JSON.stringify(select(sorted, "name", "salary")));console.log(JSON.stringify(select(orderBy(table, "salary"), "name")));Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| K must be a real column name of T. orderBy(table, "nme") would not compile. | — | — | |
| The result type contains only the selected columns. | — | — | |
| WHERE: keep Ada (120) and Cy (100). | wellPaid = Ada, Cy | — | |
| ORDER BY name, then SELECT name, salary. | wellPaid = Ada, Cy | [{"name":"Ada","salary":120},{"name":"Cy","salary":100}] | |
| Sorting by salary ascending gives Di, Bo, Cy, Ada. | wellPaid = Ada, Cy | [{"name":"Ada","salary":120},{"name":"Cy","salary":100}]
[{"name":"Di"},{"name":"Bo"},{"name":"Cy"},{"name":"Ada"}] |
Behind the scenes
How TypeScript processes this code
TypeScript checks types and emits JavaScript. Type annotations are removed before execution.
Cost and common mistakes
where and select are O(n); orderBy is O(n log n).
- The comparison a[key] < b[key] works for numbers and strings, but the compiler cannot guarantee the column is comparable. Restrict K further if needed.
- Types are checked at compile time only. Rows from a JSON file or database still need runtime checks.
- sort mutates the array, so the helper copies it first with the spread operator.
Try it yourself
How would you add a limit helper?
Reveal answer and reasoning
function limit<T>(rows: T[], count: number): T[] { return rows.slice(0, count); }