DEV Community

Nashe Omirro
Nashe Omirro

Posted on

Handling view types from supabase with typescript

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'];

Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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
  }
// ...
Enter fullscreen mode Exit fullscreen mode

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
  );
};
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)