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
NeonCop
NeonCopOP2w ago
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?
Angelelz
Angelelz2w ago
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 type
NeonCop
NeonCopOP2w ago
Thanks @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
NeonCop
NeonCopOP2w ago
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
NeonCop
NeonCopOP2w ago
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
Angelelz
Angelelz2w ago
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:
return query as any
return query as any
NeonCop
NeonCopOP2w ago
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
Angelelz
Angelelz2w ago
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 building
NeonCop
NeonCopOP2w ago
got 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!)
Angelelz
Angelelz2w ago
Happy thanksgiving
Want results from more Discord servers?
Add your server