Skip to content

Incorrect handling of union types in PostgrestResponseSuccess<T> type #688

@leohku

Description

@leohku

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions