Skip to content

fix(types): returns type casting #604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 21, 2025
20 changes: 19 additions & 1 deletion src/PostgrestBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// @ts-ignore
import nodeFetch from '@supabase/node-fetch'

import type { Fetch, PostgrestSingleResponse, PostgrestResponseSuccess } from './types'
import type {
Fetch,
PostgrestSingleResponse,
PostgrestResponseSuccess,
CheckMatchingArrayTypes,
} from './types'
import PostgrestError from './PostgrestError'

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

return res.then(onfulfilled, onrejected)
}

/**
* Override the type of the returned `data`.
*
* @typeParam NewResult - The new result type to override with
* @deprecated Use overrideTypes<yourType, { merge: false }>() method at the end of your call chain instead
*/
returns<NewResult>(): PostgrestBuilder<CheckMatchingArrayTypes<Result, NewResult>, ThrowOnError> {
return this as unknown as PostgrestBuilder<
CheckMatchingArrayTypes<Result, NewResult>,
ThrowOnError
>
}
}
7 changes: 4 additions & 3 deletions src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PostgrestBuilder from './PostgrestBuilder'
import { GetResult } from './select-query-parser/result'
import { GenericSchema } from './types'
import { GenericSchema, CheckMatchingArrayTypes } from './types'

export default class PostgrestTransformBuilder<
Schema extends GenericSchema,
Expand Down Expand Up @@ -307,18 +307,19 @@ export default class PostgrestTransformBuilder<
* Override the type of the returned `data`.
*
* @typeParam NewResult - The new result type to override with
* @deprecated Use overrideTypes<yourType, { partial: false }>() method at the end of your call chain instead
*/
returns<NewResult>(): PostgrestTransformBuilder<
Schema,
Row,
NewResult,
CheckMatchingArrayTypes<Result, NewResult>,
RelationName,
Relationships
> {
return this as unknown as PostgrestTransformBuilder<
Schema,
Row,
NewResult,
CheckMatchingArrayTypes<Result, NewResult>,
RelationName,
Relationships
>
Expand Down
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import PostgrestError from './PostgrestError'
import { ContainsNull } from './select-query-parser/types'
import { SelectQueryError } from './select-query-parser/utils'

export type Fetch = typeof fetch

Expand Down Expand Up @@ -89,3 +91,28 @@ type ConditionalSimplifyDeep<
type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown)
type BuiltIns = Primitive | void | Date | RegExp
type Primitive = null | undefined | string | number | boolean | symbol | bigint

/**
* Utility type to check if array types match between Result and NewResult.
* Returns either the valid NewResult type or an error message type.
*/
export type CheckMatchingArrayTypes<Result, NewResult> =
// If the result is a QueryError we allow the user to override anyway
Result extends SelectQueryError<string>
? NewResult
: // Otherwise, we check basic type matching (array should be override by array, object by object)
Result extends any[]
? NewResult extends any[]
? NewResult // Both are arrays - valid
: {
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'
}
: NewResult extends any[]
? {
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'
}
: // 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
? NewResult | null
: NewResult
24 changes: 24 additions & 0 deletions test/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,30 @@ test('basic select returns types override', async () => {
`)
})

test('basic select returns from builder', async () => {
const res = await postgrest
.from('users')
.select()
.eq('username', 'supabot')
.single()
.returns<{ status: 'ONLINE' | 'OFFLINE' }>()
expect(res).toMatchInlineSnapshot(`
Object {
"count": null,
"data": Object {
"age_range": "[1,2)",
"catchphrase": "'cat' 'fat'",
"data": null,
"status": "ONLINE",
"username": "supabot",
},
"error": null,
"status": 200,
"statusText": "OK",
}
`)
})

test('basic select view', async () => {
const res = await postgrest.from('updatable_view').select()
expect(res).toMatchInlineSnapshot(`
Expand Down
120 changes: 120 additions & 0 deletions test/returns.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { expectType } from 'tsd'
import { PostgrestBuilder, PostgrestClient } from '../src/index'
import { Database } from './types'
import { TypeEqual } from 'ts-expect'

const REST_URL = 'http://localhost:3000'
const postgrest = new PostgrestClient<Database>(REST_URL)

// Test returns() with different end methods
{
// Test with single()
const singleResult = await postgrest
.from('users')
.select()
.single()
.returns<{ username: string }>()
if (singleResult.error) {
throw new Error(singleResult.error.message)
}
let result: typeof singleResult.data
let expected: { username: string }
expectType<TypeEqual<typeof result, typeof expected>>(true)

// Test with maybeSingle()
const maybeSingleResult = await postgrest
.from('users')
.select()
.maybeSingle()
.returns<{ username: string }>()
if (maybeSingleResult.error) {
throw new Error(maybeSingleResult.error.message)
}
let maybeSingleResultType: typeof maybeSingleResult.data
let maybeSingleExpected: { username: string } | null
expectType<TypeEqual<typeof maybeSingleResultType, typeof maybeSingleExpected>>(true)

// Test array to non-array type casting error
const invalidCastArray = await postgrest.from('users').select().returns<{ username: string }>()
if (invalidCastArray.error) {
throw new Error(invalidCastArray.error.message)
}
let resultType: typeof invalidCastArray.data
let resultExpected: {
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'
}
expectType<TypeEqual<typeof resultType, typeof resultExpected>>(true)

// Test non-array to array type casting error
const invalidCastSingle = postgrest
.from('users')
.select()
.single()
.returns<{ username: string }[]>()
expectType<
PostgrestBuilder<
{
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'
},
false
>
>(invalidCastSingle)

// Test with csv()
const csvResult = await postgrest.from('users').select().csv().returns<string>()
if (csvResult.error) {
throw new Error(csvResult.error.message)
}
let csvResultType: typeof csvResult.data
let csvExpected: string
expectType<TypeEqual<typeof csvResultType, typeof csvExpected>>(true)

// Test with throwOnError()
const throwResult = await postgrest
.from('users')
.select()
.returns<{ username: string }[]>()
.throwOnError()
let throwResultType: typeof throwResult.data
let throwExpected: { username: string }[]
expectType<TypeEqual<typeof throwResultType, typeof throwExpected>>(true)
let throwErrorType: typeof throwResult.error
let throwErrorExpected: null
expectType<TypeEqual<typeof throwErrorType, typeof throwErrorExpected>>(true)
}

// Test returns() with nested selects and relationships
{
const result = await postgrest
.from('users')
.select('username, messages(id, content)')
.single()
.returns<{
username: string
messages: { id: number; content: string }[]
}>()
if (result.error) {
throw new Error(result.error.message)
}
let resultType: typeof result.data
let expected: {
username: string
messages: { id: number; content: string }[]
}
expectType<TypeEqual<typeof resultType, typeof expected>>(true)
}

// Test returns() with JSON operations
{
const result = await postgrest
.from('users')
.select('data->settings')
.single()
.returns<{ settings: { theme: 'light' | 'dark' } }>()
if (result.error) {
throw new Error(result.error.message)
}
let resultType: typeof result.data
let expected: { settings: { theme: 'light' | 'dark' } }
expectType<TypeEqual<typeof resultType, typeof expected>>(true)
}
Loading