Types on conditional joins
Hey everyone!
I'm struggling with maintaining type safety when adding conditional joins in Drizzle.
I have a function that builds a query with conditional joins based on parameters. Depending on the parameters, I might need to join different tables. However, TypeScript isn't correctly inferring the return type, and I have to cast it, which defeats the purpose of type safety.
Here's a simplified version of my code:
type JoinOptions = Partial<Record<'withFoo' | 'withBar', true>>;
type BaseFields = ReturnType<typeof getTableColumnAliases<typeof baseTable>>;
type FooFields = ReturnType<typeof getTableColumnAliases<typeof fooTable>>;
type BarFields = ReturnType<typeof getTableColumnAliases<typeof barTable>>;
type MyQuerySelection<T extends JoinOptions> = {
base: BaseFields;
} & (T['withFoo'] extends true ? { foo: FooFields } : {}) &
(T['withBar'] extends true ? { bar: BarFields } : {});
export function buildMyQuery<T extends JoinOptions>({
params,
joins = {} as T,
}: {
params: MyQueryParams;
joins?: T;
}) {
const selectClauses = {
base: getTableColumnAliases(baseTable),
...(joins.withFoo && { foo: getTableColumnAliases(fooTable) }),
...(joins.withBar && { bar: getTableColumnAliases(barTable) }),
};
let query = db.select(selectClauses).from(baseTable).$dynamic();
if (joins.withFoo) {
query = withFoo({ query, params });
}
if (joins.withBar) {
query = withBar({ query, params });
}
return query;
}
But even with conditional types, TypeScript doesn't infer the return type based on joins. I have to cast the return type, which isn't ideal.
Here's how I'm using the function:
const query = buildMyQuery({
params: myParams,
joins: { withFoo: true },
});
const results = await query.execute();
results.forEach((row) => {
console.log(row.foo?.someField); // Error: Property 'foo' does not exist
});
Has anyone faced this issue or have suggestions?
Thanks in advance!
10 Replies
Looked at https://discord.com/channels/1043890932593987624/1213991555271233637/1213991555271233637 and https://discord.com/channels/1043890932593987624/1128636955270324275 and neither had satisfactory answers. cc @Dan looks like you had some idea in the first thread?
You might be able to make it work by typing the result based on the JoinOptions generic
But I don't think it's possible to make it so that typescript infers it without hlep
If you put this code in drizzle.run, I can take a look and see if I can do it
Wait, you already have typed out. Just type the return of
buildMyQuery
function with MyQuerySelection
typeThanks @Angelelz ! In my real code I actually do have the return type set as PgSelectBase, like this:
function buildMyQuery<T extends JoinOptions>({
params,
joins = {} as T,
}: {
params: MyQueryParams;
joins?: T;
}): PgSelectBase<
"base",
MyQuerySelection<T>,
"partial",
MyQueryNullabilityMap<T>
{// ... same implementation } But I still end up having to cast the query to that return type using (as unknown first) to make it work. The issue seems to be that once I start chaining conditional joins, TypeScript loses track of the relationship between the joins parameter and the final query type. Will have a drizzle.run example ready soon
https://drizzle.run/c7jfxwp7k1k4afwgdspcvvo5
here you can see the type at the end does not satisfy the output, so I have to cast to unknown and then to the actual type. If I just let it infer the return type without declaring it, then it doesn't pick up on the fact that if you didn't pass in the join then that attribute won't be available
Drizzle Run
conditional join types - Drizzle Run
also realized I had some of the nullability map set to values that aren't nullable and not-null, but changing that doesn't help either
Well, typescript has its limitations
I think your solution is good. Although I don't really like this type of abstractions, you already invested the time
If you go through drizzle's codebase you'll see that it's littered with a lot of
as any
for this type of issues
There is currently no way to tell typescript the the result you're returning conforms with the declared return type
You just need to do this:
ah yeah thats unfortunate
appreciate you looking through though!
what abstractions do you feel work better?
what does TExcludedMethods do by the way? couldn't quite figure it out
This abstraction makes your code totally dependant on the library, in this case drizzle
If you wanted to use TypeORM or any other tomorrow, there is a lot to re-write
I would suggest to type out the return type explicitely without depending on drizzle's internal types. Your function is already generic, then make it return the explicit type based on the
SelectedJoins
type
Something like: Promise<MyReturnTypeBasedOnSelectedJoins<T>>
TExcludedMethods
is used to hide via types the methods that you've used already in your query when you're not using dynamic buildinggot it
awesome thanks! big fan of drizzkle
we use this query as a subquery downstream in quite a few places so the drizzle is too deep, we'll worry about a rewrite when we get there (hopefully never!)
Happy thanksgiving