Skip to content

Commit 4fef058

Browse files
committed
fix(types): returns type casting
- Allow to use returns at the end of the call chain after a .single() - Add deprecation warning to move toward explicit overrideTypes method - Add cast checking basic logic, an array should be declared if the result is an array, an object if the result is an object
1 parent a0b56aa commit 4fef058

File tree

4 files changed

+162
-4
lines changed

4 files changed

+162
-4
lines changed

src/PostgrestBuilder.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// @ts-ignore
22
import nodeFetch from '@supabase/node-fetch'
33

4-
import type { Fetch, PostgrestSingleResponse, PostgrestResponseSuccess } from './types'
4+
import type {
5+
Fetch,
6+
PostgrestSingleResponse,
7+
PostgrestResponseSuccess,
8+
CheckMatchingArrayTypes,
9+
} from './types'
510
import PostgrestError from './PostgrestError'
611

712
export default abstract class PostgrestBuilder<Result, ThrowOnError extends boolean = false>
@@ -209,4 +214,17 @@ export default abstract class PostgrestBuilder<Result, ThrowOnError extends bool
209214

210215
return res.then(onfulfilled, onrejected)
211216
}
217+
218+
/**
219+
* Override the type of the returned `data`.
220+
*
221+
* @typeParam NewResult - The new result type to override with
222+
* @deprecated Use overrideTypes<yourType, { partial: false }>() method at the end of your call chain instead
223+
*/
224+
returns<NewResult>(): PostgrestBuilder<CheckMatchingArrayTypes<Result, NewResult>, ThrowOnError> {
225+
return this as unknown as PostgrestBuilder<
226+
CheckMatchingArrayTypes<Result, NewResult>,
227+
ThrowOnError
228+
>
229+
}
212230
}

src/PostgrestTransformBuilder.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import PostgrestBuilder from './PostgrestBuilder'
22
import { GetResult } from './select-query-parser/result'
3-
import { GenericSchema } from './types'
3+
import { GenericSchema, CheckMatchingArrayTypes } from './types'
44

55
export default class PostgrestTransformBuilder<
66
Schema extends GenericSchema,
@@ -307,18 +307,19 @@ export default class PostgrestTransformBuilder<
307307
* Override the type of the returned `data`.
308308
*
309309
* @typeParam NewResult - The new result type to override with
310+
* @deprecated Use overrideTypes<yourType, { partial: false }>() method at the end of your call chain instead
310311
*/
311312
returns<NewResult>(): PostgrestTransformBuilder<
312313
Schema,
313314
Row,
314-
NewResult,
315+
CheckMatchingArrayTypes<Result, NewResult>,
315316
RelationName,
316317
Relationships
317318
> {
318319
return this as unknown as PostgrestTransformBuilder<
319320
Schema,
320321
Row,
321-
NewResult,
322+
CheckMatchingArrayTypes<Result, NewResult>,
322323
RelationName,
323324
Relationships
324325
>

src/types.ts

+22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import PostgrestError from './PostgrestError'
2+
import { SelectQueryError } from './select-query-parser/utils'
23

34
export type Fetch = typeof fetch
45

@@ -89,3 +90,24 @@ type ConditionalSimplifyDeep<
8990
type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown)
9091
type BuiltIns = Primitive | void | Date | RegExp
9192
type Primitive = null | undefined | string | number | boolean | symbol | bigint
93+
94+
/**
95+
* Utility type to check if array types match between Result and NewResult.
96+
* Returns either the valid NewResult type or an error message type.
97+
*/
98+
export type CheckMatchingArrayTypes<Result, NewResult> =
99+
// If the result is a QueryError we allow the user to override anyway
100+
Result extends SelectQueryError<string>
101+
? NewResult
102+
: // Otherwise, we check basic type matching (array should be override by array, object by object)
103+
Result extends any[]
104+
? NewResult extends any[]
105+
? NewResult // Both are arrays - valid
106+
: {
107+
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'
108+
}
109+
: NewResult extends any[]
110+
? {
111+
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'
112+
}
113+
: NewResult // Neither are arrays - valid

test/returns.test-d.ts

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { expectType } from 'tsd'
2+
import { PostgrestBuilder, PostgrestClient } from '../src/index'
3+
import { Database } from './types'
4+
import { TypeEqual } from 'ts-expect'
5+
6+
const REST_URL = 'http://localhost:3000'
7+
const postgrest = new PostgrestClient<Database>(REST_URL)
8+
9+
// Test returns() with different end methods
10+
{
11+
// Test with single()
12+
const singleResult = await postgrest
13+
.from('users')
14+
.select()
15+
.single()
16+
.returns<{ username: string }>()
17+
if (singleResult.error) {
18+
throw new Error(singleResult.error.message)
19+
}
20+
let result: typeof singleResult.data
21+
let expected: { username: string }
22+
expectType<TypeEqual<typeof result, typeof expected>>(true)
23+
24+
// Test with maybeSingle()
25+
const maybeSingleResult = await postgrest
26+
.from('users')
27+
.select()
28+
.maybeSingle()
29+
.returns<{ username: string }>()
30+
if (maybeSingleResult.error) {
31+
throw new Error(maybeSingleResult.error.message)
32+
}
33+
let maybeSingleResultType: typeof maybeSingleResult.data
34+
let maybeSingleExpected: { username: string }
35+
expectType<TypeEqual<typeof maybeSingleResultType, typeof maybeSingleExpected>>(true)
36+
37+
// Test array to non-array type casting error
38+
const invalidCastArray = (await postgrest.from('users').select().returns<{ username: string }>())
39+
.data
40+
expectType<typeof invalidCastArray>({
41+
Error:
42+
'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',
43+
})
44+
45+
// Test non-array to array type casting error
46+
const invalidCastSingle = postgrest
47+
.from('users')
48+
.select()
49+
.single()
50+
.returns<{ username: string }[]>()
51+
expectType<
52+
PostgrestBuilder<
53+
{
54+
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'
55+
},
56+
false
57+
>
58+
>(invalidCastSingle)
59+
60+
// Test with csv()
61+
const csvResult = await postgrest.from('users').select().csv().returns<string>()
62+
if (csvResult.error) {
63+
throw new Error(csvResult.error.message)
64+
}
65+
let csvResultType: typeof csvResult.data
66+
let csvExpected: string
67+
expectType<TypeEqual<typeof csvResultType, typeof csvExpected>>(true)
68+
69+
// Test with throwOnError()
70+
const throwResult = await postgrest
71+
.from('users')
72+
.select()
73+
.returns<{ username: string }[]>()
74+
.throwOnError()
75+
let throwResultType: typeof throwResult.data
76+
let throwExpected: { username: string }[]
77+
expectType<TypeEqual<typeof throwResultType, typeof throwExpected>>(true)
78+
let throwErrorType: typeof throwResult.error
79+
let throwErrorExpected: null
80+
expectType<TypeEqual<typeof throwErrorType, typeof throwErrorExpected>>(true)
81+
}
82+
83+
// Test returns() with nested selects and relationships
84+
{
85+
const result = await postgrest
86+
.from('users')
87+
.select('username, messages(id, content)')
88+
.single()
89+
.returns<{
90+
username: string
91+
messages: { id: number; content: string }[]
92+
}>()
93+
if (result.error) {
94+
throw new Error(result.error.message)
95+
}
96+
let resultType: typeof result.data
97+
let expected: {
98+
username: string
99+
messages: { id: number; content: string }[]
100+
}
101+
expectType<TypeEqual<typeof resultType, typeof expected>>(true)
102+
}
103+
104+
// Test returns() with JSON operations
105+
{
106+
const result = await postgrest
107+
.from('users')
108+
.select('data->settings')
109+
.single()
110+
.returns<{ settings: { theme: 'light' | 'dark' } }>()
111+
if (result.error) {
112+
throw new Error(result.error.message)
113+
}
114+
let resultType: typeof result.data
115+
let expected: { settings: { theme: 'light' | 'dark' } }
116+
expectType<TypeEqual<typeof resultType, typeof expected>>(true)
117+
}

0 commit comments

Comments
 (0)