-
Notifications
You must be signed in to change notification settings - Fork 485
Description
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<TableName> = {
data:
| (TableName extends "researches"
? Research[]
: TableName extends "links"
? Link[]
: TableName extends "results"
? Result[]
: any[])
| null;
error: PostgrestError | null;
};
export default function useTable(tableName: TableName) {
const supabase = useSupabaseClient();
const { isLoading, data } = useQuery({
queryKey: [tableName],
queryFn: async () => {
/* Type error here!
vvvvvvvvvvvvvvv */
const { data, error }: Response<TableName> = 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<TableName>'.
Type 'PostgrestResponseSuccess<Research | Link | Result>' is not assignable to type 'Response<TableName>'.
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'.
Property 'research' is missing in type 'Link' but required in type 'Research'.
Expected behaviour
In a nutshell, our supplied Response<TableName>
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<TableName>
.
References
Distributive Conditional Types: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types