SupaLaunch logo
SUPALAUNCH
ShowcaseDocs
SupaLaunch
HomeDemoDocumentationBlogShowcaseServices and FrameworksTerms of ServicePrivacy policy
Tools
DB Schema GeneratorStartup Idea Generator
Other Projects
Syntha AICreateChatsCron Expression GeneratorAI Prompts Generator
Contacts
Created with ❤️ by Denisdenis@supalaunch.com
  • Showcase
  • Docs
Blog

Using TypeScript with Supabase: A Practical Guide to Type Safety

20 Nov 2024
By Denis
typescriptsupabasedatabasereact

Launch your startup with our Supabase + NextJS Starter Kit

SupaLaunch is a SaaS boilerplate built using Supabase and Next.js. It includes authentication, Stripe payments, Postgres database, 20+ TailwindCSS themes, emails, OpenAI API streaming, file storage and more.

  • Save weeks of your time: No need to setup authentication, payments, emails, file storage, etc.
  • Focus on what matters: Spend your time building your product, not boilerplate code.
Get started with SupaLaunch

TypeScript with Supabase Guide

Working with databases in TypeScript can be tricky - you want to ensure your application knows exactly what data it's dealing with. Supabase makes this easy by automatically generating TypeScript types from your database schema. Let's explore how to set this up and use it effectively.

Getting Started with Type Generation

First, you'll need to install the Supabase CLI. This tool is essential for generating TypeScript types from your database schema. The CLI provides commands to interact with your Supabase project, including type generation capabilities.

  npm install supabase@">=1.8.1" --save-dev

The version 1.8.1 or higher is required as it includes important improvements to type generation. Install it as a dev dependency since we'll only use it during development.

Setting Up the CLI

Before generating types, you need to authenticate with your Supabase account and initialize your project. This process creates necessary configuration files and establishes a connection to your Supabase project:

  # Login to Supabase CLI
  npx supabase login
  
  # Initialize your project
  npx supabase init

The login command will open your browser to authenticate with Supabase. The init command creates a supabase/ directory in your project with configuration files.

Generating Database Types

There are two ways to generate types: from your remote project or local development database. Choose the method that best fits your workflow:

From Remote Project

This method pulls the schema from your deployed Supabase project. It's useful when working with an existing database or when multiple developers need to stay in sync with the production schema:

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

From Local Development

If you're developing locally with a Supabase instance, this method generates types from your local database. It's perfect for rapid development and testing schema changes:

  npx supabase gen types typescript --local > database.types.ts

Understanding Generated Types

Let's look at a practical example using a common task management system. Here's a SQL schema that defines a tasks table with common fields you might use:

  create table public.tasks (
    id bigint generated always as identity primary key,
    title text not null,
    is_completed boolean default false,
    user_id uuid references auth.users,
    created_at timestamp with time zone default timezone('utc'::text, now())
  );

When you run the type generation command, Supabase will analyze this schema and generate corresponding TypeScript types. The generated types include three main interfaces for each table: Row (for reading), Insert (for creating), and Update (for modifying records):

  export interface Database {
    public: {
      Tables: {
        tasks: {
          Row: {
            id: number
            title: string
            is_completed: boolean
            user_id: string | null
            created_at: string
          }
          Insert: {
            id?: never  // Generated column
            title: string
            is_completed?: boolean
            user_id?: string | null
            created_at?: string
          }
          Update: {
            id?: never
            title?: string
            is_completed?: boolean
            user_id?: string | null
            created_at?: string
          }
        }
      }
    }
  }

Notice how the types reflect the SQL schema:

  • Row type includes all fields as they appear in the database
  • Insert type makes optional fields that have defaults
  • Update type makes all fields optional since you might want to update only specific fields
  • The id field is marked as never for Insert and Update since it's auto-generated

Using Types in Your Application

Setting Up the Supabase Client

To start using the generated types, you need to properly configure your Supabase client. By passing the Database type as a generic parameter, you enable TypeScript to provide type checking for all database operations:

  import { createClient } from '@supabase/supabase-js'
  import { Database } from './database.types'

  const supabase = createClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

The generic parameter <Database> tells TypeScript about your database structure, enabling autocomplete and type checking for all queries.

Type-Safe Queries

Now you can write type-safe queries that TypeScript will validate at compile time. Here are some common patterns you'll use:

  // Fetching tasks
  const getTasks = async () => {
    const { data, error } = await supabase
      .from('tasks')
      .select()
    
    if (error) throw error
    
    // data is typed as Task[] automatically!
    return data
  }

  // Creating a new task
  const createTask = async (title: string) => {
    const { data, error } = await supabase
      .from('tasks')
      .insert({
        title,
        // TypeScript will ensure we're not sending invalid fields
      })
      .select()
      .single()
    
    if (error) throw error
    return data
  }

In these examples:

  • The getTasks function automatically knows the return type includes all task fields
  • createTask validates that we're only inserting valid fields
  • TypeScript will show errors if we try to access non-existent fields or pass invalid types

Working with Related Tables

When your database has relationships between tables, you'll often need to join them in queries. Here's how to handle types for related data:

  create table public.categories (
    id bigint generated always as identity primary key,
    name text not null
  );

  alter table public.tasks add column category_id bigint references public.categories;

After adding the relationship, you'll need to define types for joined queries. This ensures TypeScript understands the shape of the combined data:

  // Define the type for the joined result
  type TaskWithCategory = Database['public']['Tables']['tasks']['Row'] & {
    categories: Database['public']['Tables']['categories']['Row']
  }

  // Fetch tasks with categories
  const getTasksWithCategories = async () => {
    const { data, error } = await supabase
      .from('tasks')
      .select(`
        *,
        categories (
          id,
          name
        )
      `)
    
    if (error) throw error
    
    // data is now typed as TaskWithCategory[]
    return data
  }

The TaskWithCategory type combines the task fields with the category fields, providing full type safety for joined queries. This helps catch common errors like:

  • Accessing non-existent fields from the joined table
  • Forgetting to include necessary fields in the select statement
  • Mishandling nullable relationships

Practical Tips

Using Type Helpers

Supabase generates several type helpers that can make your code more concise and maintainable. Instead of writing out full type paths, you can use these shortcuts:

  import { Tables, Enums } from './database.types'

  // Instead of Database['public']['Tables']['tasks']['Row']
  type Task = Tables<'tasks'>
  
  // For enum types if you have any
  type UserRole = Enums<'user_role'>

These helpers provide the same type safety but with much cleaner syntax. They're especially useful when you need to reference types in multiple places throughout your codebase.

Handling Nullable Fields

TypeScript's null checking is particularly valuable when working with optional database fields. Here's how to safely handle nullable relationships:

  const updateTaskUser = async (taskId: number, userId: string | null) => {
    const { data, error } = await supabase
      .from('tasks')
      .update({ user_id: userId }) // TypeScript ensures userId can be null
      .eq('id', taskId)
      .select()
      .single()
    
    if (error) throw error
    return data
  }

This pattern ensures that:

  • You can't accidentally pass undefined where null is expected
  • TypeScript warns you when you try to use nullable fields without checking
  • The database schema's null constraints are reflected in your code

Keeping Types Updated

Manual Updates

As your database schema evolves, you'll need to keep your TypeScript types in sync. Add a convenient npm script to make this process easier:

  # Add this to your package.json scripts
  "update-types": "npx supabase gen types typescript --project-id \"$PROJECT_ID\" > database.types.ts"

Now team members can simply run npm run update-types whenever the schema changes.

Automatic Updates with GitHub Actions

For larger teams, automating type updates can prevent synchronization issues. This GitHub Action checks for schema changes daily:

  name: Update Database Types

  on:
    schedule:
      - cron: '0 0 * * *'  # Runs daily at midnight

  jobs:
    update:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v2
        - uses: actions/setup-node@v2
          with:
            node-version: '16'
        - run: npm run update-types
        - name: Commit changes
          uses: EndBug/add-and-commit@v7
          with:
            message: 'chore: update database types'
            add: 'database.types.ts'

This automation helps ensure that:

  • Types are always up-to-date with the database schema
  • Changes are automatically committed to your repository
  • Team members get notified of schema changes through git

Common Gotchas and Solutions

Handling JSON Columns

JSON columns require special attention since they can contain complex nested data structures. Here's how to properly type them:

  interface TaskMetadata {
    priority: 'low' | 'medium' | 'high'
    tags: string[]
  }

  // Extend the generated types
  declare module './database.types' {
    interface Database {
      public: {
        Tables: {
          tasks: {
            Row: {
              metadata: TaskMetadata | null
            }
            Insert: {
              metadata?: TaskMetadata | null
            }
            Update: {
              metadata?: TaskMetadata | null
            }
          }
        }
      }
    }
  }

This approach provides several benefits:

  • Strong typing for JSON column contents
  • Autocomplete for nested properties
  • Runtime type checking when parsing JSON data

Type-Safe RLS Policies

When using Row Level Security (RLS), it's important to type your auth context to ensure policy consistency:

  type AuthContext = {
    user_id: string
    role: 'user' | 'admin'
  }

  const getUserTasks = async (context: AuthContext) => {
    // RLS will use the user_id automatically
    const { data, error } = await supabase
      .from('tasks')
      .select()
    
    return { data, error }
  }

This pattern helps you:

  • Document expected authentication context
  • Ensure consistent policy implementation
  • Catch potential security issues at compile time

Conclusion

Using TypeScript with Supabase transforms database interactions into a type-safe, developer-friendly experience. The automatic type generation and rich tooling help catch errors early, improve code quality, and enhance team collaboration.

Remember these key points:

  • Generate types whenever you update your database schema
  • Use type helpers to make your code more readable
  • Set up automatic type updates for team workflows
  • Pay special attention to JSON columns and RLS policies

By following these practices, you'll create more reliable applications with fewer runtime errors and better maintainability.

Additional Resources

  • Supabase TypeScript Documentation
  • TypeScript Handbook
  • Supabase docs

These resources will help you dive deeper into specific aspects of using TypeScript with Supabase and stay updated with best practices.