Description
Bug report
- I confirm this is a bug with Supabase, not with my own application.
- I confirm I have searched the Docs, GitHub Discussions, and Discord.
Describe the bug
This is like a really dumb typescript issue that I ran into.
I wanted to subscribe to different events of table updates with one generic helper method. However, overloaded function types does not allow me to subscribe to a union of different overloaded method types making it difficult to subscribe to the table with exactly same parameters, but just different event type. I explain this issue further in the next section.
To Reproduce
Here is what I am doing in Typescript:
// Just some helper generic types
type TableChangeEvent = "*" | "INSERT" | "UPDATE" | "DELETE";
// Type for mapping Postgres change events to payload types.
type RealtimePostgresChangeEventToPayloadType<
TPayload extends { [key: string]: any },
> = {
INSERT: RealtimePostgresInsertPayload<TPayload>;
UPDATE: RealtimePostgresUpdatePayload<TPayload>;
DELETE: RealtimePostgresDeletePayload<TPayload>;
"*": RealtimePostgresUpdatePayload<TPayload>;
};
const supabaseClient = createClient(/* some credentials */);
// The problematic helper method
// This is where I am trying to subscribe to the table events:
// Generic react hook to subscribe to table events and run a callback when event occurs
export function useSubToTableChanges<
TEvent extends TableChangeEvent, // <- "*" | "INSERT" | "UPDATE" | "DELETE"
TTableSchema extends ZodTypeAny, // not relevant to understand in this issue
Payload extends
RealtimePostgresChangeEventToPayloadType<TTableSchema>[TEvent], // This maps the payload type based on Event type passed in the function.
>(
channelName: string,
event: TEvent,
tableName: string,
callback: (eventInfo?: Payload) => void,
filter?: string,
) {
const supabase = supabaseClient;
const finalCallback = (payload: Payload) => {
// Validate the payload against the Zod schema
callback(payload);
};
useEffect(() => {
const subscription: RealtimeChannel = supabase
.channel(channelName)
.on<TTableSchema>(
"postgres_changes",
{
schema: "public",
table: tableName,
event: event as any, // ignore all typescript problems
filter,
},
(e) => finalCallback(e as Payload), // also ignore as its not working
)
.subscribe();
return () => {
supabase.removeChannel(subscription);
};
}, [supabase, tableName, callback, event]);
}
The code above is a "working" code and there is technically no problem running the code above, but one annoyance is the event as any
where I am disabling typescript for that part. The problem comes from the overloaded method "on" where it is defined as follows:
/**
* The following is placed here to display on supabase.com/docs/reference/javascript/subscribe.
* @param type One of "broadcast", "presence", or "postgres_changes".
* @param filter Custom object specific to the Realtime feature detailing which payloads to receive.
* @param callback Function to be invoked when event handler is triggered.
*/
on<T extends { [key: string]: any }>(
type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL}`>,
callback: (payload: RealtimePostgresChangesPayload<T>) => void
): RealtimeChannel
on<T extends { [key: string]: any }>(
type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`>,
callback: (payload: RealtimePostgresInsertPayload<T>) => void
): RealtimeChannel
on<T extends { [key: string]: any }>(
type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`>,
callback: (payload: RealtimePostgresUpdatePayload<T>) => void
): RealtimeChannel
on<T extends { [key: string]: any }>(
type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`,
filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`>,
callback: (payload: RealtimePostgresDeletePayload<T>) => void
): RealtimeChannel
// more overloaded method below for boardcast and presence
on(
type: `${REALTIME_LISTEN_TYPES}`,
filter: { event: string; [key: string]: string },
callback: (payload: any) => void
): RealtimeChannel {
return this._on(type, filter, callback)
}
The thing that my helper function wants to do is be able to switch between these overloaded methods during "runtime" or based on the parameters passed in. Which for me requires me to make a union parameter type of all four of the overloaded methods and pass that as the parameter of the function which is what I tried to do with the code above. This is a problem for typescript because there is no overloaded method defined for such loosely typed parameter and it isn't able to just create an union of all the defined method types. Another way to solve this problem was to repeat the identical function 4 times for each event type which is technically okay for my usage, but its not optimized and not clean.
Expected behavior
The expected behavior is for me to not have to either disable type checking during call, or repeat myself four times for each overloaded method type. The best solution for me is to be able to just call like below:
const subscription: RealtimeChannel = supabase
.channel(channelName)
.on<TTableSchema>(
"postgres_changes",
{
schema: "public",
table: tableName,
event,
filter,
},
finalCallback,
)
.subscribe();
This works in Javascript but this gives us a type error as follows:
No overload matches this call.
The last overload gave the following error.
Argument of type '"postgres_changes"' is not assignable to parameter of type '"system"'.ts(2769)
Screenshots

System information
- Version of supabase-js: 2.49.9
- Version of Node.js: 20.18.3