Decoding TypeScript Internals: A Practical Approach
In this article, we’ll pull back the curtain and decode a complex type by building a simplified QueryBuilder from scratch. This example is…
Decoding TypeScript Internals: A Practical Approach

In this article, we’ll pull back the curtain and decode a complex type by building a simplified QueryBuilder from scratch. This example is a perfect showcase for how advanced TypeScript features come together to create a flexible, type-safe API. Our mission is to break down this powerful type to understand its internal workings, proving that even the most intricate type definitions are just a clever combination of core concepts.
The Final Product: A Glimpse of Type-Safety
Here’s a look at what our final product will achieve. Notice how each step in the chain provides a new level of type safety.
// Define your database model
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Our Query Builder in action
const userQuery = createQueryBuilder<User>();
const result = userQuery
.select(['id', 'name']) // The `select` method narrows the return type
.where('email', 'contains', 'example.com') // The `where` method provides context-aware operators
.orderBy('createdAt', 'asc') // The `orderBy` method ensures valid field names
.execute();
// The final result is perfectly typed!
// `result` will be a Promise<{id: number, name: string}[]>;
// If you try to access a non-selected field, TypeScript will throw an error:
// result.then(users => {
// console.log(users[0].email); // Error: 'email' does not exist on type '{id: number; name: string;}'.
// });
Pretty neat, right? Now let’s dive into how this is built.
The Core Code: The QueryBuilder Type
At the heart of our solution is the QueryBuilder type itself. It's a generic interface that tracks not just the base model (T), but also the selected fields (S).
type QueryBuilder<T extends Record<string, any>, S extends keyof T = keyof T> = {
select: <K extends keyof T>(fields: K[]) => QueryBuilder<T, K>;
where: <K extends keyof T>(
field: K,
operator: T[K] extends string ? 'equals' | 'contains' | 'startsWith' :
T[K] extends number ? 'equals' | 'gt' | 'lt' | 'gte' | 'lte' :
T[K] extends boolean ? 'equals' : never,
value: T[K]
) => QueryBuilder<T, S>;
orderBy: <K extends keyof T>(field: K, direction: 'asc' | 'desc') => QueryBuilder<T, S>;
execute: () => Promise<Pick<T, S>[]>;
};
This single type is responsible for all the magic.
function createQueryBuilder<T extends Record<string, any>>(): QueryBuilder<T> {
return {
select: <K extends keyof T>(fields: K[]) => createQueryBuilder<T>() as QueryBuilder<T, K>,
where: (field, operator, value) => createQueryBuilder<T>() as QueryBuilder<T, any>,
orderBy: (field, direction) => createQueryBuilder<T>() as QueryBuilder<T, any>,
execute: async () => {
return [] as any;
}
};
}
The above function is used to create a query builder object.
Now let’s break it down, method by method.
Decoding the QueryBuilder Type
Before we look at the methods, let’s understand the generics:
**<T extends Record<string, any>>**: This is our first generic parameter,T. It represents the full data model, like ourUserinterface. It's constrained to be an object (Record<string, any>).**<S extends keyof T = keyof T>: This is the second generic parameter,S. It represents the set of selected fields**.S extends keyof T: This constraint ensures that the selected fields are always valid keys of the original modelT.= keyof T: This is a default value. When the query builder is first created,Sdefaults tokeyof T, meaning all fields are "selected" by default.
This S generic is the key to our entire system! It's a type-level state variable that gets updated as we chain methods.
Decoding the select method
The select method is the most important part of our type-level state management.
select: <K extends keyof T>(fields: K[]) => QueryBuilder<T, K>;
**<K extends keyof T>**: This is a new, local genericK. It captures the union of field names passed to theselectmethod (e.g.,'id' | 'name'). This provides autocompletion for thefieldsarray.**QueryBuilder<T, K>: This is the core of the magic. The method returns a newQueryBuilderinstance where ourSgeneric (the selected fields) is now narrowed** toK. Any subsequent method in the chain will now operate on this narrowed type.
Decoding the where and orderBy methods
These methods add conditions to our query. They are powerful because they provide context-aware autocompletion for their arguments.
where: <K extends keyof T>(
field: K,
operator: T[K] extends string ? 'equals' | 'contains' | ... : // Conditionals
T[K] extends number ? 'equals' | 'gt' | ... :
T[K] extends boolean ? 'equals' : never,
value: T[K]
) => QueryBuilder<T, S>;
orderBy: <K extends keyof T>(field: K, direction: 'asc' | 'desc') => QueryBuilder<T, S>;
**<K extends keyof T>**: Both methods use a genericKto ensure thefieldargument is a valid key from the original modelT.- The Conditional Type: The
wheremethod'soperatorparameter uses a powerful Conditional Type. T[K] extends string ? 'equals' | 'contains' | ...: This looks at the type of the fieldT[K](e.g.,T['name']which isstring). If it's astring, TypeScript will only allow a set of string-specific operators like'contains'.- If it’s a
number, it allows number-specific operators like'gt'or'lt'. - This provides a beautiful developer experience with perfect autocompletion for operators.
**QueryBuilder<T, S>: Notice that these methods return theQueryBuilderwith the originalSgeneric**. This is because adding awhereororderByclause does not change which fields we are ultimately selecting.
Decoding the execute method
The execute method is the grand finale. This is where all the type information we've collected is finally used to define the return type.
execute: () => Promise<Pick<T, S>[]>;
**Promise<...>**: The method returns aPromise, as a query is typically an asynchronous operation.**Pick<T, S>: This is a powerful built-in Utility Type**.Picktakes two arguments: an object type (T) and a union of keys (S).- It returns a new object type that contains only the properties specified in
S. - Since our
Sgeneric was correctly narrowed by theselectmethod,Pick<T, S>gives us a new type that contains only the fields we selected!
The Implementation (For the Curious)
While the type definitions are the interesting part, here is a simplified implementation that makes it all work.
// Create the query builder
function createQueryBuilder<T extends Record<string, any>>(): QueryBuilder<T> {
return {
// The implementation uses type assertions (`as`) to satisfy the compiler
// because the runtime logic doesn't explicitly track the types.
select: <K extends keyof T>(fields: K[]) => createQueryBuilder<T>() as QueryBuilder<T, K>,
where: (field, operator, value) => createQueryBuilder<T>() as QueryBuilder<T, any>,
orderBy: (field, direction) => createQueryBuilder<T>() as QueryBuilder<T, any>,
execute: async () => {
// In a real application, this would run the query against a database
// and return the results.
return [] as any;
}
};
}
The implementation uses simple type assertions (as QueryBuilder<T, K>) to tell TypeScript, "trust me, the type of what I'm returning matches the return type definition." This is a very common pattern when building type-level libraries, where the type system performs all the checks at compile-time and the runtime code simply fulfills the contract.
Conclusion
By leveraging a few key advanced TypeScript features — generics, conditional types, indexed, literal and utility types — we were able to create a highly flexible, expressive, and completely type-safe query builder. This pattern can be applied to any API or data manipulation layer to enforce correctness at compile time, leading to more robust and maintainable code.
LinkedIn Profile: https://www.linkedin.com/in/manikandan-raman/
메타데이터
- post_id
- 4d9ac03de8a8
- slug
- decoding-typescript-internals-a-practical-approach-4d9ac03de8a8
- url
- https://medium.com/@maniyuvan446/decoding-typescript-internals-a-practical-approach-4d9ac03de8a8
- canonical_url
- https://medium.com/@maniyuvan446/decoding-typescript-internals-a-practical-approach-4d9ac03de8a8
- author_url
- https://medium.com/@maniyuvan446
- status
- ok
- fetched_at
- 2026-07-19 01:37:51