Prisma $queryRaw TS2345: 'string' not assignable to Sql
Why a NestJS PrismaService rejects a template literal in brackets, three fixes that compile and run on Prisma 7, and the two errors that come next.
TL;DR#
Prisma's $queryRaw only accepts a tagged template or a Prisma.sql object. Writing this.prismaService.$queryRaw( followed by a backtick string inside parentheses hands it a plain string, so TypeScript stops with TS2345. Remove the parentheses so the backtick string becomes a tagged template, or wrap it in Prisma.sql, or switch to $queryRawUnsafe with numbered placeholders if the SQL really is a prebuilt string. Then take the quotes off '${writerAddress}': a placeholder inside a quoted literal compiles but does not work.
The error#
The Stack Overflow question comes from a NestJS repository class that calls $queryRaw on an injected PrismaService, with the SQL written as a template literal between parentheses:
// the method from the question, shown inside its repository class
export class WorkRepositoryPostgres {
constructor(private readonly prismaService: PrismaService) {}
async findWriterNumber(writerAddress: string): Promise<number> {
const maxNumber = await this.prismaService.$queryRaw<{
max: number;
}>(
`SELECT coalesce(max('writerNumber') + 1, 0) as max
FROM "Work"
LEFT OUTER JOIN "WriterID"
ON "Work"."id" = "WorkID"."workId" AND "WorkID"."address" = '${writerAddress}'`,
);
return maxNumber[0].max;
}
}The compiler output in the question:
src/infrastructure/persistence/work.repository.postgres.ts:188:7 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'TemplateStringsArray | Sql'.The same mistake was reproduced on 7 October 2026 with Prisma 7.10.0 (prisma-client generator, @prisma/adapter-pg), PostgreSQL 18.6 in the official Docker image, and two TypeScript compilers. The repository method was rebuilt against a PrismaService class that extends PrismaClient, the way a NestJS service does, without NestJS itself installed. TypeScript 5.9.3 prints the message from the question word for word; TypeScript 7.0.2 prints the two union members in the opposite order, so search for both:
src/broken.ts(7,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'TemplateStringsArray | Sql'.
src/broken.ts(7,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Sql | TemplateStringsArray'.Why the parentheses turn the query into a string#
$queryRaw is declared in the generated client with this signature (copied from src/generated/prisma/internal/class.ts in the test project, with the same query parameter type as the one in the Prisma raw queries documentation):
155: $queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;The method is "implemented as a tagged template", as the Prisma docs put it. JavaScript has two different syntaxes that look almost the same:
fn`SELECT … ${x}`is a tagged template. JavaScript callsfnwith an array of the literal pieces (aTemplateStringsArray) and passes each${…}value as a separate argument. Prisma turns those values into numbered bind parameters and sends a prepared statement.fn(`SELECT … ${x}`)is an ordinary function call whose argument is an untagged template literal. JavaScript evaluates the literal into one finishedstringbefore the call, with the value already pasted into the SQL text.
The second form is exactly what the parameter type forbids, and for a good reason: by the time Prisma received that string, it could no longer tell which part was SQL and which part was writerAddress. TypeScript reports the mismatch at the call site, which is why the error points at the line of the backtick, not at the database. NestJS plays no part in it: a PrismaService that extends PrismaClient, the usual NestJS pattern, inherits the method and its signature unchanged.
The same error appears when the SQL is assembled in a variable first and then passed in, because a const sql = '…' is a string too:
src/prebuilt.ts(7,63): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Sql | TemplateStringsArray'.Fix#
All three versions below compiled cleanly on both TypeScript versions and returned a result from PostgreSQL in the test. Each fix also takes the quotes off the placeholder, for the reason explained in step 4.
1. Call $queryRaw as a tag (drop the parentheses)#
This is the accepted answer's fix. Apart from indentation, the only change from the broken test file is the missing ( and ) around the backtick string:
6,11c6,10
< const maxNumber = await prismaService.$queryRaw<{ max: number }[]>(
< `SELECT coalesce(max("writerNumber") + 1, 0) AS max
< FROM "Work"
< LEFT OUTER JOIN "WorkID"
< ON "Work"."id" = "WorkID"."workId" AND "WorkID"."address" = ${writerAddress}`,
< );
---
> const maxNumber = await prismaService.$queryRaw<{ max: number }[]>`
> SELECT coalesce(max("writerNumber") + 1, 0) AS max
> FROM "Work"
> LEFT OUTER JOIN "WorkID"
> ON "Work"."id" = "WorkID"."workId" AND "WorkID"."address" = ${writerAddress}`;In a NestJS repository the method becomes:
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class WorkRepositoryPostgres {
constructor(private readonly prismaService: PrismaService) {}
async findWriterNumber(writerAddress: string): Promise<number> {
const maxNumber = await this.prismaService.$queryRaw<{ max: number }[]>`
SELECT coalesce(max("writerNumber") + 1, 0) AS max
FROM "Work"
LEFT OUTER JOIN "WorkID"
ON "Work"."id" = "WorkID"."workId" AND "WorkID"."address" = ${writerAddress}`;
return maxNumber[0].max;
}
}Only the call syntax and the type argument changed; the @Injectable() class around it is ordinary NestJS and was not part of the test.
2. Keep the parentheses, wrap the SQL in Prisma.sql#
The second answer on the question uses the Prisma.sql helper, which builds a Sql object from a tagged template. Use it when the query is built in one place and run in another:
import { Prisma } from './generated/prisma/client';
const query = Prisma.sql`
SELECT coalesce(max("writerNumber") + 1, 0) AS max
FROM "Work"
LEFT OUTER JOIN "WorkID"
ON "Work"."id" = "WorkID"."workId" AND "WorkID"."address" = ${writerAddress}`;
const maxNumber = await this.prismaService.$queryRaw<{ max: number }[]>(query);With the Prisma 7 prisma-client generator, Prisma comes from your generated client's client file (the test imported it from there). The docs' examples import it from @prisma/client, which is where the older prisma-client-js generator puts it.
Prisma.sql fragments also nest, which covers the usual reason for building SQL as a string — an optional WHERE:
const where = writerAddress
? Prisma.sql`WHERE wi."address" = ${writerAddress}`
: Prisma.empty;
const rows = await this.prismaService.$queryRaw<{ max: number }[]>`
SELECT coalesce(max(w."writerNumber") + 1, 0) AS max
FROM "Work" w JOIN "WorkID" wi ON wi."workId" = w."id" ${where}`;The value inside the nested fragment is still sent as a bind parameter.
3. The SQL really is a string: $queryRawUnsafe with positional parameters#
If the text comes from a file or a query builder, $queryRawUnsafe is the method typed for it (query: string). Keep the values out of the string and pass them as extra arguments, numbered $1, $2 for PostgreSQL, as in the parameterised queries section of the Prisma docs:
const sql = `SELECT coalesce(max("writerNumber") + 1, 0) AS max
FROM "Work"
LEFT OUTER JOIN "WorkID"
ON "Work"."id" = "WorkID"."workId" AND "WorkID"."address" = $1`;
const maxNumber = await this.prismaService.$queryRawUnsafe<{ max: number }[]>(sql, writerAddress);The "Unsafe" in the name is about string building: the Prisma docs on $queryRawUnsafe warn that putting user input into the string itself opens the query to SQL injection. Positional parameters avoid that.
4. Take the quotes off the placeholder#
Both answers on the question keep '${writerAddress}' in single quotes, although the accepted answer's own explanation says the prepared statement sends the value as a separate parameter that needs no quotation marks. That explanation is the one to follow. Once the code compiles, a quoted placeholder is no longer a type error but a runtime one: inside quotes, PostgreSQL reads the placeholder as plain text, and Prisma sends one more value than the statement uses. With the quotes kept, the test query (same tables, fixed column names) failed on Prisma 7.10.0 in both forms:
tagged, quoted placeholder -> THROWS P2010 Raw query failed. Code: `08P01`. Message: `bind message supplies 1 parameters, but prepared statement "" requires 0`
Prisma.sql, quoted placeholder -> THROWS P2010 Raw query failed. Code: `08P01`. Message: `bind message supplies 1 parameters, but prepared statement "" requires 0`Older Prisma versions behave differently with a quoted placeholder, including silently using the wrong value; the whole story, with intervals and LIKE patterns, is in Prisma $queryRaw fails with variables inside single quotes.
Verify the fix#
Run npx tsc --noEmit first: the TS2345 line should be gone. Then call the method against data you know. The test seeded two works — writerNumber 1 for address 0xaaa and 5 for 0xbbb — and ran the three fixes with writerAddress = '0xaaa':
fix 1 tagged template -> [{"max":6}]
fix 2 Prisma.sql -> [{"max":6}]
fix 3 $queryRawUnsafe + $1 -> [{"max":6}]All three forms return the same row. To see the SQL, turn on query logging with log: ['query'] in the PrismaClient options: the address should appear as a bare $1, never as '$1' or as the literal value.
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.
The two errors hiding behind TS2345#
The query from the question has two more errors that only show up once the first one is gone, and one result worth checking.
maxNumber[0] on a single-object type. $queryRaw returns an array, one object per row, and the type argument T is the type of that whole result. The question types it as { max: number }, then indexes it with [0]. Under strict, after removing the parentheses, both compilers reject that line:
src/wrongtype.ts(11,10): error TS7053: Element implicitly has an 'any' type because expression of type '0' can't be used to index type '{ max: number; }'.Write the array type, $queryRaw<{ max: number }[]>, as in the fixes above. With strict set to false, the same file compiled without any error in the test, and the type annotation simply describes the wrong shape.
max('writerNumber') with single quotes. In PostgreSQL, single quotes make a string constant, not a column name, so the aggregate runs over the text writerNumber and the + 1 fails. Run on its own, the expression from the question gives:
max('writerNumber') quoted column -> THROWS P2010 Raw query failed. Code: `42883`. Message: `operator does not exist: text + integer`Quoted identifiers use double quotes: max("writerNumber"). The double quotes are needed because the column name is mixed case: in the test schema, a Prisma field writerNumber without @map produced a column of exactly that name, and PostgreSQL folds unquoted names to lower case.
Check what the LEFT JOIN counts. With the address condition in the ON clause, a LEFT OUTER JOIN keeps every row of "Work", so the maximum is taken over all works. In the test, 0xaaa only owns the work with number 1, yet the query returned 6 (the global maximum, 5, plus one). If the intent is the next number for this writer, filter with an inner join instead:
const rows = await this.prismaService.$queryRaw<{ max: number }[]>`
SELECT coalesce(max(w."writerNumber") + 1, 0) AS max
FROM "Work" w
JOIN "WorkID" wi ON wi."workId" = w."id"
WHERE wi."address" = ${writerAddress}`;inner join on address -> [{"max":2}]
inner join, unknown address -> [{"max":0}]The coalesce(…, 0) now does its job for an address with no works yet.
When the result is a count: BigInt#
max("writerNumber") + 1 on an Int column comes back as a JavaScript number. A count(*) does not: PostgreSQL's count aggregate returns a bigint, and Prisma's raw query type mapping turns it into a BigInt. Any code that passes those rows through JSON.stringify — a controller returning them as JSON, for instance — then throws:
rows -> [ { n: 2n } ]
JSON.stringify -> TypeError: Do not know how to serialize a BigInt
count(*)::int -> [{"n":2}]Cast in SQL (count(*)::int) when the number fits in 32 bits, and type the row to match.
If the next error is about the connection#
If the next failure is about the connection rather than the SQL: on Prisma 6 with Supabase's pooled port, a raw query that fails with prepared statement "s0" already exists is a pooler collision, explained in the prepared statement s0 fix. If Prisma reports an invalid port number although the port is right, a special character in the password is breaking DATABASE_URL: see Prisma can't connect to PostgreSQL.
The rule behind TS2345 is short: $queryRaw must see the backticks itself. Let it tag the template, or hand it a Prisma.sql object, and keep every value as a whole, unquoted placeholder. When the SQL truly is a string, $queryRawUnsafe with positional parameters is the honest signature for it.
Further reading: Prisma deleteMany: Foreign key constraint failed (P2003). Also see Keystone 6 Foreign Key NOT NULL on a Relationship Field.
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
Prisma $queryRaw Fails With Variables Inside Single Quotes
Why interval and LIKE patterns break in Prisma raw SQL, the rewrites that work on Prisma 6 and 7, and how to prove the query now uses your value.
Fix Prisma v7 paths[1] must be of type string error
A step-by-step fix for the Prisma 7 startup crash, reproduced against Postgres, plus the driver-adapter error that follows it.
Prisma nested connect: syntax error at or near "ON"
Find out in one query whether your production PostgreSQL is too old for Prisma, then move the data to a supported server with pg_dump and pg_restore.


