Skip to content

Commit 963bc7e

Browse files
committedFeb 19, 2025
feat(types): add overrideTypes method
Allow both partial and complete override of the return type, do partial override by default
1 parent a479b87 commit 963bc7e

File tree

4 files changed

+243
-14
lines changed

4 files changed

+243
-14
lines changed
 

‎src/PostgrestBuilder.ts

+41
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
PostgrestSingleResponse,
77
PostgrestResponseSuccess,
88
CheckMatchingArrayTypes,
9+
MergePartialResult,
10+
IsValidResultOverride,
911
} from './types'
1012
import PostgrestError from './PostgrestError'
1113

@@ -227,4 +229,43 @@ export default abstract class PostgrestBuilder<Result, ThrowOnError extends bool
227229
ThrowOnError
228230
>
229231
}
232+
233+
/**
234+
* Override the type of the returned `data` field in the response.
235+
*
236+
* @typeParam NewResult - The new type to cast the response data to
237+
* @typeParam Options - Optional type configuration (defaults to { merge: true })
238+
* @typeParam Options.merge - When true, merges the new type with existing return type. When false, replaces the existing types entirely (defaults to true)
239+
* @example
240+
* ```typescript
241+
* // Merge with existing types (default behavior)
242+
* const query = supabase
243+
* .from('users')
244+
* .select()
245+
* .overrideTypes<{ custom_field: string }>()
246+
*
247+
* // Replace existing types completely
248+
* const replaceQuery = supabase
249+
* .from('users')
250+
* .select()
251+
* .overrideTypes<{ id: number; name: string }, { merge: false }>()
252+
* ```
253+
* @returns A PostgrestBuilder instance with the new type
254+
*/
255+
overrideTypes<
256+
NewResult,
257+
Options extends { merge?: boolean } = { merge: true }
258+
>(): PostgrestBuilder<
259+
IsValidResultOverride<Result, NewResult, true, false, false> extends true
260+
? MergePartialResult<NewResult, Result, Options>
261+
: CheckMatchingArrayTypes<Result, NewResult>,
262+
ThrowOnError
263+
> {
264+
return this as unknown as PostgrestBuilder<
265+
IsValidResultOverride<Result, NewResult, true, false, false> extends true
266+
? MergePartialResult<NewResult, Result, Options>
267+
: CheckMatchingArrayTypes<Result, NewResult>,
268+
ThrowOnError
269+
>
270+
}
230271
}

‎src/types.ts

+52-14
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unk
9292
type BuiltIns = Primitive | void | Date | RegExp
9393
type Primitive = null | undefined | string | number | boolean | symbol | bigint
9494

95+
export type IsValidResultOverride<Result, NewResult, Ok, ErrorResult, ErrorNewResult> =
96+
Result extends any[]
97+
? NewResult extends any[]
98+
? // Both are arrays - valid
99+
Ok
100+
: ErrorResult
101+
: NewResult extends any[]
102+
? ErrorNewResult
103+
: // Neither are arrays - valid
104+
// Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
105+
ContainsNull<Result> extends true
106+
? Ok | null
107+
: Ok
108+
95109
/**
96110
* Utility type to check if array types match between Result and NewResult.
97111
* Returns either the valid NewResult type or an error message type.
@@ -100,19 +114,43 @@ export type CheckMatchingArrayTypes<Result, NewResult> =
100114
// If the result is a QueryError we allow the user to override anyway
101115
Result extends SelectQueryError<string>
102116
? NewResult
103-
: // Otherwise, we check basic type matching (array should be override by array, object by object)
104-
Result extends any[]
105-
? NewResult extends any[]
106-
? NewResult // Both are arrays - valid
107-
: {
117+
: IsValidResultOverride<
118+
Result,
119+
NewResult,
120+
NewResult,
121+
{
108122
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'
123+
},
124+
{
125+
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'
109126
}
110-
: NewResult extends any[]
111-
? {
112-
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'
113-
}
114-
: // Neither are arrays - valid
115-
// Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
116-
ContainsNull<Result> extends true
117-
? NewResult | null
118-
: NewResult
127+
>
128+
129+
type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T
130+
131+
type MergeDeep<New, Row> = {
132+
[K in keyof New | keyof Row]: K extends keyof Row
133+
? K extends keyof New
134+
? IsPlainObject<New[K]> extends true
135+
? IsPlainObject<Row[K]> extends true
136+
? MergeDeep<New[K], Row[K]>
137+
: Row[K]
138+
: Row[K]
139+
: Row[K]
140+
: K extends keyof New
141+
? New[K]
142+
: never
143+
}
144+
145+
// Helper to check if a type is a plain object (not an array)
146+
type IsPlainObject<T> = T extends any[] ? false : T extends object ? true : false
147+
148+
// Merge the new result with the original (Result) when partial is true.
149+
// If NewResult is an array, merge each element.
150+
export type MergePartialResult<NewResult, Result, Options> = Options extends { merge: true }
151+
? Result extends any[]
152+
? NewResult extends any[]
153+
? Array<Simplify<MergeDeep<NewResult[number], Result[number]>>>
154+
: never
155+
: Simplify<MergeDeep<NewResult, Result>>
156+
: NewResult

‎test/basic.ts

+24
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,30 @@ test('basic select returns from builder', async () => {
140140
`)
141141
})
142142

143+
test('basic select overrideTypes from builder', async () => {
144+
const res = await postgrest
145+
.from('users')
146+
.select()
147+
.eq('username', 'supabot')
148+
.single()
149+
.overrideTypes<{ status: 'ONLINE' | 'OFFLINE' }>()
150+
expect(res).toMatchInlineSnapshot(`
151+
Object {
152+
"count": null,
153+
"data": Object {
154+
"age_range": "[1,2)",
155+
"catchphrase": "'cat' 'fat'",
156+
"data": null,
157+
"status": "ONLINE",
158+
"username": "supabot",
159+
},
160+
"error": null,
161+
"status": 200,
162+
"statusText": "OK",
163+
}
164+
`)
165+
})
166+
143167
test('basic select view', async () => {
144168
const res = await postgrest.from('updatable_view').select()
145169
expect(res).toMatchInlineSnapshot(`

‎test/override-types.test-d.ts

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { expectType } from 'tsd'
2+
import { TypeEqual } from 'ts-expect'
3+
import { PostgrestClient } from '../src'
4+
import { CustomUserDataType, Database } from './types'
5+
6+
const REST_URL = 'http://localhost:54321'
7+
const postgrest = new PostgrestClient<Database>(REST_URL)
8+
9+
// Test merge array result to object should error
10+
{
11+
const singleResult = await postgrest
12+
.from('users')
13+
.select()
14+
.overrideTypes<{ custom_field: string }>()
15+
if (singleResult.error) {
16+
throw new Error(singleResult.error.message)
17+
}
18+
let result: typeof singleResult.data
19+
expectType<
20+
TypeEqual<
21+
typeof result,
22+
{
23+
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'
24+
}
25+
>
26+
>(true)
27+
}
28+
29+
// Test merge object result to array type should error
30+
{
31+
const singleResult = await postgrest
32+
.from('users')
33+
.select()
34+
.single()
35+
.overrideTypes<{ custom_field: string }[]>()
36+
if (singleResult.error) {
37+
throw new Error(singleResult.error.message)
38+
}
39+
let result: typeof singleResult.data
40+
expectType<
41+
TypeEqual<
42+
typeof result,
43+
{
44+
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'
45+
}
46+
>
47+
>(true)
48+
}
49+
50+
// Test with single() / maybeSingle()
51+
{
52+
const singleResult = await postgrest
53+
.from('users')
54+
.select()
55+
.single()
56+
.overrideTypes<{ custom_field: string }>()
57+
if (singleResult.error) {
58+
throw new Error(singleResult.error.message)
59+
}
60+
let result: typeof singleResult.data
61+
expectType<TypeEqual<(typeof result)['custom_field'], string>>(true)
62+
}
63+
// Test with maybeSingle()
64+
{
65+
const maybeSingleResult = await postgrest
66+
.from('users')
67+
.select()
68+
.maybeSingle()
69+
.overrideTypes<{ custom_field: string }>()
70+
if (maybeSingleResult.error) {
71+
throw new Error(maybeSingleResult.error.message)
72+
}
73+
let maybeSingleResultType: typeof maybeSingleResult.data
74+
let expectedType: { custom_field: string } | null
75+
expectType<TypeEqual<typeof maybeSingleResultType, typeof expectedType>>(true)
76+
}
77+
// Test replacing behavior
78+
{
79+
const singleResult = await postgrest
80+
.from('users')
81+
.select()
82+
.single()
83+
.overrideTypes<{ custom_field: string }, { merge: false }>()
84+
if (singleResult.error) {
85+
throw new Error(singleResult.error.message)
86+
}
87+
let result: typeof singleResult.data
88+
expectType<TypeEqual<typeof result, { custom_field: string }>>(true)
89+
}
90+
91+
// Test with select()
92+
{
93+
const singleResult = await postgrest
94+
.from('users')
95+
.select()
96+
.overrideTypes<{ custom_field: string }[]>()
97+
if (singleResult.error) {
98+
throw new Error(singleResult.error.message)
99+
}
100+
let result: typeof singleResult.data
101+
expectType<
102+
TypeEqual<
103+
typeof result,
104+
{
105+
username: string
106+
data: CustomUserDataType | null
107+
age_range: unknown
108+
catchphrase: unknown
109+
status: 'ONLINE' | 'OFFLINE' | null
110+
custom_field: string
111+
}[]
112+
>
113+
>(true)
114+
}
115+
// Test replacing select behavior
116+
{
117+
const singleResult = await postgrest
118+
.from('users')
119+
.select()
120+
.overrideTypes<{ custom_field: string }[], { merge: false }>()
121+
if (singleResult.error) {
122+
throw new Error(singleResult.error.message)
123+
}
124+
let result: typeof singleResult.data
125+
expectType<TypeEqual<typeof result, { custom_field: string }[]>>(true)
126+
}

0 commit comments

Comments
 (0)
Please sign in to comment.