postgrest-js
postgrest-js copied to clipboard
Incorrect handling of union types in `PostgrestResponseSuccess<T>` type
Bug report
Describe the bug
await supabase.from(tableName).select("*")
returns a type PostgrestResponse<T>
, which could be of type PostgrestResponseSuccess<T>
.
Currently PostgrestResponseSuccess<T>
is defined as:
interface PostgrestResponseSuccess<T> extends PostgrestResponseBase {
error: null;
data: T[];
count: number | null;
}
Note that the generic type T
above has no handling of union types. For example, for PostgrestResponseSuccess<A | B>
, data
is of type (A | B)[]
, when it should be of type A[] | B[]
.
This is problematic because (A | B)[]
, an array that contains both type A
and B
entries, isn't a valid return type for a database SELECT
query, while the correct type, A[] | B[]
, generates a type error.
To Reproduce
To illustrate, consider a generic useTable
hook which takes in tableName
and returns a Tanstack Query useQuery
hook that loads a table from Supabase.
import { useQuery } from "@tanstack/react-query";
import { useSupabaseClient } from "@supabase/auth-helpers-react";
import { PostgrestError } from "@supabase/supabase-js";
// Database types
interface Research {
id: string;
research: string;
}
interface Link {
id: string;
link: string;
}
interface Result {
id: string;
result: string;
}
type TableName = "researches" | "links" | "results";
type Response = {
data: Research[] | Link[] | Result[] | null;
error: PostgrestError | null;
};
type Query<T> = {
isLoading: boolean;
data:
| (T extends "researches"
? Research[]
: T extends "links"
? Link[]
: T extends "results"
? Result[]
: any[])
| null | undefined;
}
export default function useTable<T extends TableName>(tableName: T) {
const supabase = useSupabaseClient();
const { isLoading, data }: Query<T> = useQuery({
queryKey: [tableName],
queryFn: async () => {
/* Type error here!
vvvvvvvvvvvvvvv */
const { data, error }: Response = await supabase
.from(tableName)
.select("*");
if (error) throw error;
return data;
},
});
return { isLoading, data };
}
The full type error generated is as follows:
Type 'PostgrestResponse<Research | Link | Result>' is not assignable to type 'Response'.
Type 'PostgrestResponseSuccess<Research | Link | Result>' is not assignable to type 'Response'.
Types of property 'data' are incompatible.
Type '(Research | Link | Result)[]' is not assignable to type 'Research[] | Link[] | Result[] | null'.
Type '(Research | Link | Result)[]' is not assignable to type 'Research[]'.
Type 'Research | Link | Result' is not assignable to type 'Research'.
Expected behaviour
In a nutshell, our supplied Response
generates the correct type, Research[] | Link[] | Result[] | null
, which is what we expect returned from the database.
However, Typescript tries to assign it to (Research | Link | Result)[]
, an array that mixes Research
, Link
, and Result
entries. This should, in no circumstance, be returned from the database. Hence, the type handling here is incorrect.
The fix
The fix is to allow the generic type T
of PostgrestResponseSuccess<T>
to be transformed into a distributive type if T
is a union type. This can be implemented by adding a type ToArray<T>
, as follows:
type ToArray<T> = T extends any ? T[] : never;
interface PostgrestResponseSuccess<T> extends PostgrestResponseBase {
error: null;
data: ToArray<T>;
count: number | null;
}
With the fix in place, PostgrestResponseSuccess<Research | Link | Result>
can be assigned correctly to Response
.
References
Distributive Conditional Types: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types