Skip to content

Commit af43282

Browse files
samuelho-devclaude
andcommitted
fix: add JsonValue short-circuit to prevent TS2589 on recursive type fields
JsonValue is a recursive type that causes TypeScript to hit depth limits when ExtractInsertType/ExtractUpdateType/StripKyselyWrapper try to evaluate conditional checks. Adding a JsonValue guard before the `extends` check avoids expanding the recursive type entirely. Also fixes pre-existing exactOptionalPropertyTypes error in dmmf-mocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 010ae7e commit af43282

File tree

5 files changed

+127
-20
lines changed

5 files changed

+127
-20
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 5.3.3
4+
5+
### Patch Changes
6+
7+
- Fix TS2589 "Type instantiation is excessively deep" for schemas with JsonValue fields
8+
9+
Added short-circuit guards in `ExtractInsertType`, `ExtractUpdateType`, and `StripKyselyWrapper` type utilities to avoid expanding the recursive `JsonValue` type before evaluating conditional type checks. This fixes the TS2589 error when using `Selectable<T>`, `Insertable<T>`, or `Updateable<T>` on schemas containing `JsonValue | null` fields (e.g., Prisma `Json?` columns).
10+
311
## 5.3.2
412

513
### Patch Changes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "prisma-effect-kysely",
3-
"version": "5.3.2",
3+
"version": "5.3.3",
44
"description": "Prisma generator that creates Effect Schema types from Prisma schema compatible with Kysely",
55
"license": "MIT",
66
"author": "Samuel Ho",

src/__tests__/deep-type-instantiation.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ import type {
3232
UpdateQueryBuilder,
3333
} from 'kysely';
3434
import { describe, it, expectTypeOf } from 'vitest';
35-
import { columnType, generated, type ColumnType, type Generated } from '../kysely/helpers';
35+
import {
36+
columnType,
37+
generated,
38+
JsonValue,
39+
type ColumnType,
40+
type Generated,
41+
type Selectable,
42+
type Insertable,
43+
type Updateable,
44+
} from '../kysely/helpers';
3645

3746
// ============================================================================
3847
// Simulate a realistically complex database schema (20+ fields per table)
@@ -290,3 +299,85 @@ describe('Deep type instantiation - multiple tables simultaneously', () => {
290299
expectTypeOf<AllSelectables>().toHaveProperty('payment');
291300
});
292301
});
302+
303+
// ============================================================================
304+
// JsonValue field tests - TS2589 regression prevention
305+
// ============================================================================
306+
// JsonValue is a recursive type. Without short-circuit guards in
307+
// ExtractInsertType / ExtractUpdateType / StripKyselyWrapper,
308+
// TypeScript tries to fully expand the recursive type before evaluating
309+
// the conditional — hitting the depth limit.
310+
311+
const PaymentWithJson = Schema.Struct({
312+
id: columnType(PaymentId, Schema.Never, Schema.Never),
313+
order_id: OrderId,
314+
amount: Schema.Number,
315+
currency: Schema.String,
316+
status: generated(Schema.String),
317+
last_payment_error: Schema.NullOr(JsonValue),
318+
metadata: Schema.NullOr(JsonValue),
319+
raw_response: JsonValue,
320+
created_at: generated(Schema.DateFromSelf),
321+
updated_at: generated(Schema.DateFromSelf),
322+
});
323+
324+
interface JsonTestDB {
325+
payment_with_json: Schema.Schema.Type<typeof PaymentWithJson>;
326+
}
327+
328+
describe('Deep type instantiation - JsonValue fields (TS2589 regression)', () => {
329+
it('should resolve Selectable<PaymentWithJson> without TS2589', () => {
330+
type Select = Selectable<typeof PaymentWithJson>;
331+
332+
expectTypeOf<Select>().toHaveProperty('id');
333+
expectTypeOf<Select>().toHaveProperty('order_id');
334+
expectTypeOf<Select>().toHaveProperty('last_payment_error');
335+
expectTypeOf<Select>().toHaveProperty('metadata');
336+
expectTypeOf<Select>().toHaveProperty('raw_response');
337+
});
338+
339+
it('should resolve Insertable<PaymentWithJson> without TS2589', () => {
340+
type Insert = Insertable<typeof PaymentWithJson>;
341+
342+
// id should be excluded (never insert)
343+
expectTypeOf<Insert>().not.toHaveProperty('id');
344+
345+
// order_id should be required
346+
expectTypeOf<Insert>().toHaveProperty('order_id');
347+
348+
// JsonValue fields should be present
349+
expectTypeOf<Insert>().toHaveProperty('last_payment_error');
350+
expectTypeOf<Insert>().toHaveProperty('metadata');
351+
expectTypeOf<Insert>().toHaveProperty('raw_response');
352+
});
353+
354+
it('should resolve Updateable<PaymentWithJson> without TS2589', () => {
355+
type Update = Updateable<typeof PaymentWithJson>;
356+
357+
// id should be excluded (never update)
358+
expectTypeOf<Update>().not.toHaveProperty('id');
359+
360+
// JsonValue fields should be present
361+
expectTypeOf<Update>().toHaveProperty('last_payment_error');
362+
expectTypeOf<Update>().toHaveProperty('metadata');
363+
expectTypeOf<Update>().toHaveProperty('raw_response');
364+
});
365+
366+
it('should resolve KyselySelectable with JsonValue fields without TS2589', () => {
367+
type Select = KyselySelectable<JsonTestDB['payment_with_json']>;
368+
369+
expectTypeOf<Select>().toHaveProperty('id');
370+
expectTypeOf<Select>().toHaveProperty('last_payment_error');
371+
expectTypeOf<Select>().toHaveProperty('metadata');
372+
expectTypeOf<Select>().toHaveProperty('raw_response');
373+
});
374+
375+
it('should resolve KyselyInsertable with JsonValue fields without TS2589', () => {
376+
type Insert = KyselyInsertable<JsonTestDB['payment_with_json']>;
377+
378+
expectTypeOf<Insert>().not.toHaveProperty('id');
379+
expectTypeOf<Insert>().toHaveProperty('order_id');
380+
expectTypeOf<Insert>().toHaveProperty('last_payment_error');
381+
expectTypeOf<Insert>().toHaveProperty('raw_response');
382+
});
383+
});

src/__tests__/helpers/dmmf-mocks.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,18 @@ export function createMockModel(overrides: Partial<DMMF.Model> & { name: string
8484
*/
8585
export function createMockField(overrides: Partial<DMMF.Field> & { name: string }): DMMF.Field {
8686
return {
87-
name: overrides.name,
88-
kind: overrides.kind ?? 'scalar',
89-
type: overrides.type ?? 'String',
90-
isList: overrides.isList ?? false,
91-
isRequired: overrides.isRequired ?? true,
92-
isUnique: overrides.isUnique ?? false,
93-
isId: overrides.isId ?? false,
94-
isReadOnly: overrides.isReadOnly ?? false,
95-
hasDefaultValue: overrides.hasDefaultValue ?? false,
96-
relationName: overrides.relationName,
97-
documentation: overrides.documentation,
98-
nativeType: overrides.nativeType,
99-
dbName: overrides.dbName ?? null,
100-
isGenerated: overrides.isGenerated ?? false,
101-
isUpdatedAt: overrides.isUpdatedAt ?? false,
87+
kind: 'scalar',
88+
type: 'String',
89+
isList: false,
90+
isRequired: true,
91+
isUnique: false,
92+
isId: false,
93+
isReadOnly: false,
94+
hasDefaultValue: false,
95+
nativeType: null,
96+
dbName: null,
97+
isGenerated: false,
98+
isUpdatedAt: false,
99+
...overrides,
102100
};
103101
}

src/kysely/helpers.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,11 @@ const extractParametersFromTypeLiteral = (
457457
* with unique symbols, which can cause type matching failures when TypeScript
458458
* compiles from source files with different symbol references.
459459
*/
460-
type ExtractInsertType<T> = T extends { readonly __insert__: infer I } ? I : T;
460+
type ExtractInsertType<T> = T extends JsonValue
461+
? T
462+
: T extends { readonly __insert__: infer I }
463+
? I
464+
: T;
461465

462466
/**
463467
* Check if a type is nullable (includes null or undefined).
@@ -487,7 +491,11 @@ type ExtractInsertBaseType<T> = ExtractInsertType<T>;
487491
* with unique symbols, which can cause type matching failures when TypeScript
488492
* compiles from source files with different symbol references.
489493
*/
490-
type ExtractUpdateType<T> = T extends { readonly __update__: infer U } ? U : T;
494+
type ExtractUpdateType<T> = T extends JsonValue
495+
? T
496+
: T extends { readonly __update__: infer U }
497+
? U
498+
: T;
491499

492500
/**
493501
* Custom Insertable type that:
@@ -553,7 +561,9 @@ type StripColumnTypeWrapper<T> = T extends {
553561
* Order matters: check Generated first, then ColumnType.
554562
* Preserves branded foreign keys (UserId, ProductId, etc.).
555563
*/
556-
type StripKyselyWrapper<T> = StripColumnTypeWrapper<StripGeneratedWrapper<T>>;
564+
type StripKyselyWrapper<T> = T extends JsonValue
565+
? T
566+
: StripColumnTypeWrapper<StripGeneratedWrapper<T>>;
557567

558568
/**
559569
* Strip Kysely wrappers from all fields in a type.

0 commit comments

Comments
 (0)