Handling view types from supabase with typescript
Tags: typescript, webdev, tutorial
Published: 2023-06-13 05:56:17

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:

```ts
// 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:

```ts
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:
```ts
// ...
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:
```ts
// 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:
```ts
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:
```ts
// 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']`.

```ts
// detail has `assignedId` and `assignedName`
if (hasAssigned(detail)) {
  // ...
  // typescript won't complain
  if (hasFilepath(detail)) {
    // ...
  }
}
```