Drizzle TableLikeHasEmptySelection error in generic helpers
Keep your reusable Drizzle query helpers compiling on 0.45.4: why only generic code breaks, four workarounds that type-check, and what each one costs you.
TL;DR#
From drizzle-orm 0.39.0 onwards, .from() and the join methods check every subquery for an empty selection with a conditional type. When the argument's type depends on a generic type parameter (a helper declared as <T extends PgSelect>(qb: T), or <TTable extends ...>(table: TTable)), TypeScript cannot resolve that check, and the call fails with TS2345. The query itself is fine: the SQL it builds runs correctly. On the current stable line, widen the argument at the call site (qb.as('sq') as Subquery, or the table union instead of a type parameter). The real fix is listed in Drizzle's open rc.5 pull request and compiles in the rc5 preview build.
The report is drizzle-orm issue #4069, opened against 0.39.1. In the editor, the error reads:
Argument of type SubqueryWithSelection<T["_"]["selectedFields"], "__subquery__"> is not assignable to parameter of type 'TableLikeHasEmptySelection<SubqueryWithSelection<T["_"]["selectedFields"], "__subquery__">> extends true ? DrizzleTypeError<...> : SubqueryWithSelection<...>'.ts(2345)People who pass a generic table rather than a subquery hit the same check, as the issue comments report. Reproduced here with the helper from those comments, the message is spelled out in full:
src/generic-table.ts(11,27): error TS2345: Argument of type 'TTable' is not assignable to parameter of type 'TableLikeHasEmptySelection<TTable> extends true ? DrizzleTypeError<"Cannot reference a data-modifying statement subquery if it doesn't contain a `returning` clause"> : TTable'.The trigger is the type parameter, not $dynamic()#
The issue title blames dynamic queries, and the maintainers first answered that they could not reproduce it. Both are explained by one detail: the error message in the report mentions T["_"]["selectedFields"], so the subquery was built inside a generic function. The snippet in the issue, pasted as is with concrete types, compiles.
This was checked in a throwaway Docker container (node:22-bookworm-slim, TypeScript 5.9.3) with three files against the same users and systems tables:
- concrete.ts: the issue's snippet,
db.select(...).from(subquery.as('__subquery__'))wheresubqueryends in$dynamic(). - generic-subquery.ts: the same count wrapped in a reusable helper, the pattern the dynamic query building docs teach for
withPagination:
import { sql } from 'drizzle-orm';
import type { PgSelect } from 'drizzle-orm/pg-core';
import { drizzle } from 'drizzle-orm/postgres-js';
const db = drizzle.mock();
export function countRows<T extends PgSelect>(qb: T) {
return db
.select({ count: sql<number>`cast(count(*) as int)` })
.from(qb.as('__subquery__'));
}- generic-table.ts: the variant from the comments, a helper typed
<TTable extends NamedTables>(table: TTable, name: string)that callsdb.select().from(table).
On 0.38.4 all three compile. On 0.45.4, the current latest release, only the concrete file passes:
===== drizzle-orm 0.45.4 / typescript Version 5.9.3 =====
$ npx tsc -p . --noEmit
exit code: 2
src/generic-subquery.ts(11,11): error TS2345: Argument of type 'SubqueryWithSelection<T["_"]["selectedFields"], "__subquery__">' is not assignable to parameter of type 'TableLikeHasEmptySelection<SubqueryWithSelection<T["_"]["selectedFields"], "__subquery__">> extends true ? DrizzleTypeError<...> : SubqueryWithSelection<...>'.
src/generic-table.ts(11,27): error TS2345: Argument of type 'TTable' is not assignable to parameter of type 'TableLikeHasEmptySelection<TTable> extends true ? DrizzleTypeError<"Cannot reference a data-modifying statement subquery if it doesn't contain a `returning` clause"> : TTable'.
[… 2 indented detail lines of the generic-table error trimmed]The same errors, for the generic subquery and the generic table helper, appear on 0.39.0, on 1.0.0-rc.4 and with TypeScript 7.0.2. A generic subquery passed to leftJoin fails the same way.
Why the check breaks on generic arguments#
Pull request #3976, merged on 23 January 2025, let PostgreSQL insert, update and delete statements act as CTEs. A data-modifying statement without returning selects nothing, so referencing it in a from makes no sense. To refuse that at compile time, the PR added a type named TableLikeHasEmptySelection. In the published 0.45.4 declarations (pg-core/query-builders/select.types.d.ts) it reads:
export type TableLikeHasEmptySelection<T extends PgTable | Subquery | PgViewBase | SQL> = T extends Subquery ? Equal<T['_']['selectedFields'], {}> extends true ? true : false : false;The source parameter of from is declared as TableLikeHasEmptySelection<TFrom> extends true ? DrizzleTypeError<...> : TFrom, where TFrom is the type of the argument you pass.
With a concrete table or subquery, TypeScript evaluates the condition, gets false, and the parameter type collapses to the argument's own type. With a generic argument it cannot. The TypeScript release notes that introduced conditional types describe this rule: "A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables." The parameter therefore stays an unresolved conditional type, and TTable (or the subquery built from T) is not assignable to it. That is why the error message repeats the whole conditional instead of naming a concrete mismatch. The join methods (PgSelectJoinFn) carry the same check, which is why joins break too.
Fix it on the current release#
Each of the four workarounds below compiled on drizzle-orm 0.45.4 with TypeScript 5.9.3 and 7.0.2. On 5.9.3 they were also called from a call site, and the emitted declarations were read to see what the caller gets back. They all work the same way: the value that reaches .from() no longer has a type that depends on a type parameter. They differ in what type information survives.
- Generic subquery: cast it to
Subquery. Keep the generic helper and widen only the argument:
import { sql, type Subquery } from 'drizzle-orm';
import type { PgSelect } from 'drizzle-orm/pg-core';
import { drizzle } from 'drizzle-orm/postgres-js';
const db = drizzle.mock();
export function countRows<T extends PgSelect>(qb: T) {
return db
.select({ count: sql<number>`cast(count(*) as int)` })
.from(qb.as('__subquery__') as Subquery);
}The cost: the subquery's own columns and alias are no longer typed. The emitted declaration shows the table name as string. This is safe when the outer select names its fields explicitly, as the count does. Because the widened type no longer carries the subquery's columns, it suits helpers like this one that never reference them.
- Generic subquery: drop the type parameter. If the helper does not need to return a type derived from
T, take the base type directly:
import { sql } from 'drizzle-orm';
import type { PgSelect } from 'drizzle-orm/pg-core';
import { drizzle } from 'drizzle-orm/postgres-js';
const db = drizzle.mock();
export function countRows(qb: PgSelect) {
return db
.select({ count: sql<number>`cast(count(*) as int)` })
.from(qb.as('__subquery__'));
}- Generic table: take the union, not a type parameter.
import { eq } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/postgres-js';
import { systems, users } from './schema';
type NamedTables = typeof users | typeof systems;
const db = drizzle.mock();
export function findByName(table: NamedTables, name: string) {
return db.select().from(table).where(eq(table.name, name));
}- Generic table: keep the parameter, widen inside the call with
.from(table as NamedTables). This is useful when the type parameter is used elsewhere in the function.
The cost of options 3 and 4 is the reason people wrote a generic in the first place. The rows come back typed as the union of every table's row shape, not as the row of the table you passed. For tables that share a shape, as here, that is harmless. For tables with different columns, you will have to narrow the result yourself.
Avoid the two shortcuts suggested in the thread unless you are stuck: an as any cast turns off type checking for that argument, and pinning 0.38.4 keeps you on a release from before the CTE work and the fixes that came after it.
Verify the fix#
First, confirm the problem was only ever in the types. The same helper, written as plain JavaScript and run with postgres-js 3.4.9 against PostgreSQL 17.11 in a second container, sent the expected SQL and returned the right counts. It also worked with a where added to the dynamic query afterwards:
select cast(count(*) as int) from (select "id" from "users") "__subquery__"
[ { count: 3 } ]
sql: 'select cast(count(*) as int) from (select "id" from "users" where "users"."name" = $1) "__subquery__"',
[ { count: 1 } ]Then run the type check on the files you changed. After the workaround, npx tsc --noEmit should exit with code 0 and print no TS2345 line. If your helper is consumed by other code, emit declarations (npx tsc --declaration --emitDeclarationOnly) and read the return type. For a count helper you should still see count: number in the row type.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When the fix ships#
Drizzle maintainer Sukairo-02 wrote in the issue on 27 August 2026: "Reproduced and fixed. Fix will land in upcoming rc.5 release". The changelog of the open rc.5 pull request #5966 lists "Fixed subquery derived from .$dynamic() query builder being unusable with .from, .*Join (fixes #4069)". That pull request targets the beta branch, the 1.0 line, and no 0.x release contains the fix. On npm, the rc5 dist-tag points to a preview build, 1.0.0-rc.5-5935859. Against that build, the generic subquery, generic table and generic join files all compiled:
===== drizzle-orm 1.0.0-rc.5-5935859 / typescript Version 5.9.3 =====
$ npx tsc -p . --noEmit
exit code: 0A preview build is not a release, and the same pull request lists breaking changes for postgres-js JSON parameters and relational query filters. Keep the workaround on 0.45.x, and remove it once you upgrade to an rc.5 or 1.0 release and the type check passes without it.
Moving to the 1.0 line brings other changes. Relational Queries v2 also resolves an unrelated Drizzle crash, Offset is outside the bounds of the DataView on PostGIS columns. If your generic helpers also run inside transactions, check how a swallowed tx.rollback() can still commit before you refactor them. Drizzle's types have caused editor trouble before: extremely slow IntelliSense on large schemas was a type-graph cost problem with its own fix, and it has nothing to do with this check. If what you are seeing is TS2345 from a raw SQL call in Prisma rather than Drizzle, the cause is different: Prisma's $queryRaw rejects a plain string where it expects a tagged template.
The short version: the Drizzle query is correct, and TypeScript cannot evaluate the library's safety check on a generic type. Make the argument concrete at the one call that fails, write down which type information you gave up, and remove the cast once you upgrade to a release that contains the rc.5 fix.
Further reading: Drizzle: Object literal may only specify known properties.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Drizzle: Object literal may only specify known properties
Get .values() and .set() accepting your nullable Drizzle columns again: which versions lose them, why a loose tsconfig is the trigger, and the workaround that does not help.
Drizzle: Offset is outside the bounds of the DataView
Make findFirst({ with }) return your PostGIS points again: why only nested relations crash, and three fixes tested against PostGIS 3.6.
Drizzle: Cannot destructure property 'resourceArn' fix
Get drizzle-kit studio and migrate talking to Aurora through the AWS Data API again, and know which error comes next on current releases.


