Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/PostgrestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
IsValidResultOverride,
} from './types'
import PostgrestError from './PostgrestError'
import { ContainsNull } from './select-query-parser/types'

export default abstract class PostgrestBuilder<Result, ThrowOnError extends boolean = false>
implements
Expand Down Expand Up @@ -257,14 +258,20 @@ export default abstract class PostgrestBuilder<Result, ThrowOnError extends bool
NewResult,
Options extends { merge?: boolean } = { merge: true }
>(): PostgrestBuilder<
IsValidResultOverride<Result, NewResult, true, false, false> extends true
? MergePartialResult<NewResult, Result, Options>
IsValidResultOverride<Result, NewResult, false, false> extends true
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
ContainsNull<Result> extends true
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
: MergePartialResult<NewResult, Result, Options>
: CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
> {
return this as unknown as PostgrestBuilder<
IsValidResultOverride<Result, NewResult, true, false, false> extends true
? MergePartialResult<NewResult, Result, Options>
IsValidResultOverride<Result, NewResult, false, false> extends true
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
ContainsNull<Result> extends true
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
: MergePartialResult<NewResult, Result, Options>
: CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
>
Expand Down
51 changes: 36 additions & 15 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,16 @@ type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unk
type BuiltIns = Primitive | void | Date | RegExp
type Primitive = null | undefined | string | number | boolean | symbol | bigint

export type IsValidResultOverride<Result, NewResult, Ok, ErrorResult, ErrorNewResult> =
export type IsValidResultOverride<Result, NewResult, ErrorResult, ErrorNewResult> =
Result extends any[]
? NewResult extends any[]
? // Both are arrays - valid
Ok
true
: ErrorResult
: NewResult extends any[]
? ErrorNewResult
: // Neither are arrays - valid
// Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
ContainsNull<Result> extends true
? Ok | null
: Ok

true
/**
* Utility type to check if array types match between Result and NewResult.
* Returns either the valid NewResult type or an error message type.
Expand All @@ -117,33 +113,49 @@ export type CheckMatchingArrayTypes<Result, NewResult> =
: IsValidResultOverride<
Result,
NewResult,
NewResult,
{
Error: 'Type mismatch: Cannot cast array result to a single object. Use .returns<Array<YourType>> for array results or .single() to convert the result to a single object'
},
{
Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain'
}
>
> extends infer ValidationResult
? ValidationResult extends true
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
ContainsNull<Result> extends true
? NewResult | null
: NewResult
: // contains the error
ValidationResult
: never

type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T

type MergeDeep<New, Row> = {
[K in keyof New | keyof Row]: K extends keyof New
// Extract only explicit (non-index-signature) keys.
type ExplicitKeys<T> = {
[K in keyof T]: string extends K ? never : K
}[keyof T]

type MergeExplicit<New, Row> = {
// We merge all the explicit keys which allows merge and override of types like
// { [key: string]: unknown } and { someSpecificKey: boolean }
[K in ExplicitKeys<New> | ExplicitKeys<Row>]: K extends keyof New
? K extends keyof Row
? // Check if the override is on a embeded relation (array)
? Row[K] extends SelectQueryError<string>
? New[K]
: // Check if the override is on a embedded relation (array)
New[K] extends any[]
? Row[K] extends any[]
? Array<Simplify<MergeDeep<NonNullable<New[K][number]>, NonNullable<Row[K][number]>>>>
: New[K]
: // Check if both properties are objects omiting a potential null union
: // Check if both properties are objects omitting a potential null union
IsPlainObject<NonNullable<New[K]>> extends true
? IsPlainObject<NonNullable<Row[K]>> extends true
? // If they are, use the new override as source of truth for the optionality
ContainsNull<New[K]> extends true
? // If the override want to preserve optionality
? // If the override wants to preserve optionality
Simplify<MergeDeep<NonNullable<New[K]>, NonNullable<Row[K]>>> | null
: // If the override want to enforce non-null result
: // If the override wants to enforce non-null result
Simplify<MergeDeep<New[K], NonNullable<Row[K]>>>
: New[K] // Override with New type if Row isn't an object
: New[K] // Override primitives with New type
Expand All @@ -153,6 +165,15 @@ type MergeDeep<New, Row> = {
: never
}

type MergeDeep<New, Row> = Simplify<
MergeExplicit<New, Row> &
// Intersection here is to restore dynamic keys into the merging result
// eg:
// {[key: number]: string}
// or Record<string, number | null>
(string extends keyof Row ? { [K: string]: Row[string] } : {})
>

// Helper to check if a type is a plain object (not an array)
type IsPlainObject<T> = T extends any[] ? false : T extends object ? true : false

Expand Down
117 changes: 115 additions & 2 deletions test/override-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
let result: typeof singleResult.data
expectType<TypeEqual<(typeof result)['custom_field'], string>>(true)
}
// Test with maybeSingle()
// Test with maybeSingle() merging with new field
{
const maybeSingleResult = await postgrest
.from('users')
Expand All @@ -71,7 +71,50 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
throw new Error(maybeSingleResult.error.message)
}
let maybeSingleResultType: typeof maybeSingleResult.data
let expectedType: { custom_field: string } | null
let expectedType: {
age_range: unknown
catchphrase: unknown
data: CustomUserDataType | null
status: 'ONLINE' | 'OFFLINE' | null
username: string
custom_field: string
} | null
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
}
// Test with maybeSingle() merging with override field
{
const maybeSingleResult = await postgrest
.from('users')
.select()
.maybeSingle()
.overrideTypes<{ catchphrase: string }>()
if (maybeSingleResult.error) {
throw new Error(maybeSingleResult.error.message)
}
let maybeSingleResultType: typeof maybeSingleResult.data
let expectedType: {
age_range: unknown
catchphrase: string
data: CustomUserDataType | null
status: 'ONLINE' | 'OFFLINE' | null
username: string
} | null
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
}
// Test with maybeSingle() replace with override field
{
const maybeSingleResult = await postgrest
.from('users')
.select()
.maybeSingle()
.overrideTypes<{ catchphrase: string }, { merge: false }>()
if (maybeSingleResult.error) {
throw new Error(maybeSingleResult.error.message)
}
let maybeSingleResultType: typeof maybeSingleResult.data
let expectedType: {
catchphrase: string
} | null
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
}
// Test replacing behavior
Expand Down Expand Up @@ -203,6 +246,8 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
foo: number
bar: { baz: number }
en: 'ONE' | 'TWO' | 'THREE'
record: Record<string, Json | undefined> | null
recordNumber: Record<number, Json | undefined> | null
qux: boolean
}
age_range: unknown
Expand Down Expand Up @@ -232,6 +277,8 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
foo: number
bar: { baz: number }
en: 'ONE' | 'TWO' | 'THREE'
record: Record<string, Json | undefined> | null
recordNumber: Record<number, Json | undefined> | null
qux: boolean
} | null
age_range: unknown
Expand Down Expand Up @@ -299,6 +346,46 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
foo: string
bar: { baz: number; newBaz: string }
en: 'FOUR' // Overridden enum value
record: Record<string, Json | undefined> | null
recordNumber: Record<number, Json | undefined> | null
}
age_range: unknown
catchphrase: unknown
status: 'ONLINE' | 'OFFLINE' | null
}[]
>
>(true)
}

// Test merging with Json defined as Record
{
const result = await postgrest
.from('users')
.select()
.overrideTypes<{ data: { record: { baz: 'foo' }; recordNumber: { bar: 'foo' } } }[]>()
if (result.error) {
throw new Error(result.error.message)
}
let data: typeof result.data
expectType<
TypeEqual<
typeof data,
{
username: string
data: {
foo: string
bar: {
baz: number
}
en: 'ONE' | 'TWO' | 'THREE'
record: {
[x: string]: Json | undefined
baz: 'foo'
}
recordNumber: {
[x: number]: Json | undefined
bar: 'foo'
}
}
age_range: unknown
catchphrase: unknown
Expand Down Expand Up @@ -435,3 +522,29 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
>
>(true)
}

// Test overrideTypes single object with error embeded relation
{
const result = await postgrest.from('users').select('*, somerelation(*)').overrideTypes<
{
somerelation: { created_at: Date; data: string }
}[]
>()
if (result.error) {
throw new Error(result.error.message)
}
let data: typeof result.data
expectType<
TypeEqual<
typeof data,
{
username: string
data: CustomUserDataType | null
age_range: unknown
catchphrase: unknown
status: 'ONLINE' | 'OFFLINE' | null
somerelation: { created_at: Date; data: string }
}[]
>
>(true)
}
2 changes: 2 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type CustomUserDataType = {
baz: number
}
en: 'ONE' | 'TWO' | 'THREE'
record: Record<string, Json | undefined> | null
recordNumber: Record<number, Json | undefined> | null
}

export type Database = {
Expand Down
Loading