TypeScript helps us build more robust, maintainable and safe applications. In this lesson, we look at installing the Supabase CLI, and using it to generate type definitions for Supabase.
We can use this to add TypeScript support to our Supabase client, which flows through our entire application - server and client. This means we get in-editor feedback about what we can and can't do with Supabase, helping to discover cool new things, while reducing bugs.
Additionally, we use the LoaderArgs
type signature for our Loader function, which allows us to infer the return type in our component.
Note: this does not automatically update with changes to Supabase. You need to manually run this command whenever you change the structure of the database.
Generate TypeScript definitions from Supabase
supabase gen types typescript --project-id your-project-id > db_types.ts
Create typesafe Supabase client
import { createClient } from "@supabase/supabase-js";
import type { Database } from "db_types";
export default createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
Fetch data with end-to-end type defs
import { useLoaderData } from "@remix-run/react";
import supabase from "utils/supabase";
import type { LoaderArgs } from "@remix-run/node";
export const loader = async ({}: LoaderArgs) => {
const { data } = await supabase.from("messages").select();
return { messages: data ?? [] };
};
export default function Index() {
const { messages } = useLoaderData<typeof loader>();
return <pre>{JSON.stringify(messages, null, 2)}</pre>;
}
Instructor: [0:00] Our Remix application is a TypeScript project, but right now, we have very little type safety. If I have a look at data in our component, it's being inferred as any and in our loader, TypeScript has no idea what Supabase is or that this is a table or that we're selecting columns and therefore, what our data is going to be. [0:17] We can automatically generate TypeScript types from our Supabase database using the Supabase CLI. Copy the installation command that's specific to your operating system and then head back over to your terminal and paste it in. If you already have the Supabase CLI installed, you can replace install with upgrade to upgrade the package to the latest version.
[0:37] Once that's finished, we can use the Supabase CLI to login and generate an access token. Let's copy this URL and navigate to it in our browser. We can then generate a new token. My token name is going to be chatter and now we can copy this value back over to our terminal and paste it in.
[0:57] Now that our Supabase CLI is authorized to use our Supabase account, we can use it to generate TypeScript definitions for our specific project.
[1:05] Let's say Supabase Gen for generate, our types for TypeScript by providing it our project-id. We can find that in our .env file as it is the part of our Supabase URL, that's after HTTPS but before .supabase.co. I'm going to copy this value and paste it into my command here.
[1:28] We then use the greater than symbol to say we want to write these types to a file and our file is going to be called db_types.ts. Now, back in our project, we can see that that db_types file has been created and has an interface for our database, which has all of our tables and columns from Supabase.
[1:48] That's something to keep in mind, is this won't automatically stay up to date with our Supabase instance. Anytime we make structural changes in Supabase, we need to rerun this command. Now that we have all our TypeScript types for Supabase declared, we can use that in our Supabase utility file.
[2:05] We can import the type of database from db_types and then the create client, which is currently just returning a Supabase client of any public any can actually take in a type. Here, this would be our database.
[2:23] Now, when we check the type of our client, it's returning us a properly typed Supabase client with our messages and each of our columns. If we head back over to index.tsx, we can see that our Supabase client is now properly typed and from knows that it can only select from messages and select knows that we can only select an ID created at or content.
[2:43] If we have a look at the data that we get back, it knows that it's either this shape of data or .
[2:50] If we have a look at the data that we're getting back by calling the use loaded data hook, we can see that this is still being implicitly set to any. We can fix this by updating the signature of our loader function and saying that our parameters are going to be an object of loader.ogs.
[3:05] This is a type that we can pull in from at remixrun/node. I'm just going to fix up this input to say that it's importing a type and then put it below the other inputs. We can now tell the use loaded data function, what kind of type to expect, which is going to be a type of loader. This is going to infer the return type of this loader above.
[3:27] See that it's giving us back this data from super base, which is then going to pipe through the correct type to the data object that we get in our component. Now, I don't like that this data object could be . If we have a collection of messages, then I want to know that we have an array of messages. Otherwise I would expect this to be an empty array.
[3:43] Back up in our loader, I want to return an object that has the key messages, either set to the data that we get back from super base or to an empty array. Now we'll see this has lit up red because it knows that data no longer exists. We're expecting to get back messages, so we can update the value that we're destructuring from here, and the value we're rendering out from our component to be messages.
[4:07] If we go back to the browser and refresh, we should see that nothing has changed.