When you have a view on supabase and grab the types using the supabase cli, you'll soon find out that all the properties are nullable, this is not a problem with supabase but a limitation of postgres.
Solution
The easiest way to handle these types is to use type guards, but it could get a bit messy and complicated if we don't use utility types so lets make our lives easier by writing them:
// the Database type generated by supabase
import type { Database } from "./db";
export type DBTypes = {
[P in keyof Database['public']['Tables']]: Database['public']['Tables'][P]['Row'];
} & {
[P in keyof Database['public']['Views']]: Database['public']['Views'][P]['Row'];
};
// example
❌ Database['public']['Tables']['my_table']['Row'];
✅ DBTypes['my_table'];
the type above allows you to view the singular row type of any table or view just so that we can make things more readable. Some other utility types that will be helpful is ones that remove the null type from an objects properties, here we have two:
type RemoveNullOn<T, O extends keyof T = never> = {
[P in keyof T]: P extends O ? Exclude<T[P], null> : T[P];
};
type RemoveNullExcept<T, E extends keyof T = never> = {
[P in keyof T]: P extends E ? T[P] : Exclude<T[P], null>;
};
Both of these two do the same thing but in slightly different ways. With these types, we can easily create type guards more easier.
Example
I have a view in one of my projects that joins a few tables so that I could just do one api call:
// ...
class_details: {
Row: {
advisor_id: string | null
advisor_name: string | null
assigned_id: string | null
assigned_name: string | null
class_id: string | null
class_name: string | null
filepath: string | null
is_locked: boolean | null
section_grade: number | null
section_id: string | null
section_name: string | null
updated_at: string | null
}
// ...
I know that some columns will always be present, so I could then create a type guard checking for those properties else I will throw an error:
// I want to remove null from X except for these keys
export type ClassDetail = RemoveNullExcept<
DBTypes['class_details'],
'assigned_id' | 'assigned_name' | 'advisor_id' | 'advisor_name' | 'filepath' | 'updated_at'
>;
export const isValidClassDetails = (
details: DBTypes['class_details'][]
): details is ClassDetail[] => {
return details.every(
({ class_id, class_name, is_locked, section_grade, section_id, section_name }) =>
class_id && class_name && is_locked && section_grade && section_id && section_name
);
};
And to check for other properties, we could create additional type guards:
export const hasAssigned = (
detail: DBTypes['class_details']
): detail is RemoveNullOn<typeof detail, 'assigned_id' | 'assigned_name'> =>
Boolean(detail.assigned_id && detail.assigned_name);
export const hasAdvisor = (
detail: DBTypes['class_details']
): detail is RemoveNullOn<typeof detail, 'advisor_name' | 'advisor_id'> =>
Boolean(detail.advisor_id && detail.advisor_name);
export const hasFilepath = (
detail: DBTypes['class_details']
): detail is RemoveNullOn<typeof detail, 'filepath' | 'updated_at'> =>
Boolean(detail.filepath && detail.updated_at);
The neat thing about this is that DBTypes['class_details']
can always be extended by any type we've removed null on:
// true
{ a: string, b: string } extends
{ a: string | null, b: string | null } ?
true :
false
so we won't get any type errors if we nest them as long as the object type can extend the original type DBTypes['class_details']
.
// detail has `assignedId` and `assignedName`
if (hasAssigned(detail)) {
// ...
// typescript won't complain
if (hasFilepath(detail)) {
// ...
}
}
Top comments (0)