supabase / postgrest-js

Isomorphic JavaScript client for PostgREST.
https://supabase.com
MIT License
967 stars 130 forks source link

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

Open leohku opened 1 year ago

leohku commented 1 year ago

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