Declare Global Intersection Types for Transformed Supabase Data with Typescript

Jon Meyers
InstructorJon Meyers
Share this video with your friends

Social Share Links

Send Tweet
Published a year ago
Updated a year ago

TypeScript reduces runtime errors and makes code more maintainable. In this lesson, we use the Supabase CLI to introspect our PostgreSQL schema and generate TypeScript definitions.

Additionally, we resolve a collection of TypeScript errors and create a custom union type for our transformed tweets - making it globally available throughout our Next.js application.

Code Snippets

Custom type for TweetWithAuthor

type TweetWithAuthor = Tweet & {
  author: Profile;
  likes: number;
  user_has_liked_tweet: boolean;
};

Generate TypeScript definitions

npx supabase gen types typescript --project-id your-project-id > lib/database.types.ts

Resources

Lecturer: Our application is working correctly, but we've got a few TypeScript errors. Our likes don't know about this user ID property, and our likes component is being passed a tweet, but we haven't told it what type that tweet is.

Let's start by fixing our likes.userID. This is because we've added this likes table, but we haven't generated our new types from Supabase. We can run our Supabase CLI command to generate TypeScript types for our project, and now TypeScript knows what a like is, and knows that there is this user ID field.

We're not actually using these other columns. On our select statement, we can say the only column we care about is user _ID. Now, we're not over-fetching that data and just throwing it away. We only have the user ID, which is what we're using to determine whether our signed-in user has liked a particular tweet.

Now something else that feels a little bit clunky is whenever we're referencing the author of a particular tweet, we're saying tweet.profiles, which is a little bit strange. Firstly, profiles is plural, whereas our profile is a singular, and really, this is our tweet's author.

Again, where we're selecting our tweet, we can use an alias to say we want this field to be author rather than profiles. Anywhere we're referencing our tweet.profiles, we can update to be tweet.author, much clearer. Now we need to tell our likes component about the shape of our transformed tweet.

We have our transformed values for user has liked tweet and likes, and then all of the columns of the profiles table. Let's start by declaring a new type in our likes component for tweet with author. This is going to be a union type between our tweet and then our author field, which is the type of profile.

We have our custom type for likes, which is a number, and user has liked tweet, which is a boolean. We just need the definition of a tweet and a profile. Because both of these map to tables in our database, we can get them from our database type. Let's say type tweet equals database.

In here, you can see we have a public field, then tables, then the name of our table. Its row represents a single item in that table. For our tweet, we want to dig into public, then tables, then our tweets table and get the type of our row.

We can do the same thing for profiles, which gives us back our profile. Now our tweet with author contains all of the properties for our transformed tweet. The type for our tweet prop is going to be a tweet with author. Now our likes component is happy, but page.tsx is now red.

While this error is not very clear by saying that author could be null, it's that when we reach across to join this profiles table, it can't tell from our schema whether this is a single profile or an array of profiles. We can fix this here when we're transforming our data, and say that the author field is a tweet.author as profile, which will just tell TypeScript, don't worry about it, we got this.

More correctly, we could use array.is array to check whether our tweet.author is an array of values or singular. If it is an array, then we just want to grab the first author. Tweet.author index zero. Otherwise if we already have the singular, we just want tweet.author. Now when we save no more red, TypeScript is happy.

Since we now know that our author field is always going to be a single profile, we can get rid of our optional chaining here, making our code much more readable. The last thing we want to do with our types is make this tweet withAuthor type globally available so we can use it throughout our application without needing to declare it over and over again.

Let's take all of this and move it across to our global.d.ts file. We want to paste this below our import and then move our tweet withAuthor type inside our declare global. Lastly, because we're aliasing our database as DB, we just need to update our tweet and our profile to also use DB.

Now our tweet withAuthor type is globally available across our application without us needing to declare it or import it. When we created our Supabase client, we forgot to tell it about our database types, so we can pass it our database. Now, we're back to 100 percent type safety across our entire application.

Jon Meyers
Jon Meyersinstructor
~ a year ago

Correction: this is an intersection type, not a union! 🧠

https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types

Ika Pkhakadze
Ika Pkhakadze
~ a year ago

small suggestion: Supabase provides a helper type reference table row types: Tables<"tweets"> instead of DB["public"]["tables"]["tweets"]...

Markdown supported.
Become a member to join the discussionEnroll Today