Corsair
Concepts

TypeScript Concepts

Full end-to-end type safety from database to UI

TypeScript Concepts

Why TypeScript Matters for Corsair

Corsair is built on a fundamental principle: if there are type errors, something is guaranteed to be wrong.

Type errors don't guarantee everything is right, but they're incredibly effective at catching what's wrong before it reaches production.

The goal: Find and minimize errors as early as possible, ideally at compile time rather than runtime.


The Developer Experience Advantage

TypeScript intellisense is what makes Corsair feel magical to use.

Instead of memorizing APIs, guessing field names, or constantly checking documentation, you get:

  • Autocomplete for every query, mutation, and plugin
  • Inline documentation showing what each field does
  • Real-time error detection as you type
  • Refactoring confidence when changing schemas or configurations

This is what separates a good tool from a great one.


Configuration Type Safety

Defining Your Slack Channels

import { type CorsairConfig } from 'corsair'

export const config = {
  dbType: 'postgres',
  orm: 'prisma',
  framework: 'nextjs',
  pathToCorsairFolder: './corsair',
  apiEndpoint: process.env.NEXT_PUBLIC_CORSAIR_API_ROUTE!,
  db,
  connection: process.env.DATABASE_URL!,
  plugins: {
    slack: {
      token: process.env.SLACK_TOKEN!,
      channels: {
        'general': 'C01ABC123',
        'technology': 'C02DEF456',
        'notifications-error': 'C03GHI789',
      },
    },
  },
} satisfies CorsairConfig<typeof db>

export type Config = typeof config

Using Slack Channels with Intellisense

Now when you create a mutation that sends a Slack message:

const sendMessage = useCorsairMutation('send slack message to channel')

await sendMessage.mutateAsync({
  channel: '|', // <- Autocomplete shows: 'general', 'technology', 'notifications-error'
  message: 'Deployment successful!',
})

What you get:

  • Autocomplete shows only the channels you've configured
  • TypeScript errors if you try to use a non-existent channel
  • No typos like 'generl' or '#general' make it to production

The Ripple Effect of Configuration Changes

Scenario: You decide to remove the 'technology' channel from your config.

Before removal:

plugins: {
  slack: {
    channels: {
      'general': 'C01ABC123',
      'technology': 'C02DEF456',  // <- Remove this
      'notifications-error': 'C03GHI789',
    },
  },
}

After removal:

plugins: {
  slack: {
    channels: {
      'general': 'C01ABC123',
      'notifications-error': 'C03GHI789',
    },
  },
}

Immediately, TypeScript shows errors everywhere that channel is used:

// ❌ Type error: '"technology"' is not assignable to type '"general" | "notifications-error"'
await sendMessage.mutateAsync({
  channel: 'technology',
  message: 'New feature deployed!',
})

Benefits:

  • Find every usage instantly — no grep needed
  • Fix errors at compile time — not when users complain
  • Prevent silent failures — messages don't disappear into the void

Updating Channel IDs

Scenario: Slack admin recreates the 'general' channel, giving it a new ID.

Update in one place:

plugins: {
  slack: {
    channels: {
      'general': 'C04NEW123',  // <- Changed from 'C01ABC123'
      'notifications-error': 'C03GHI789',
    },
  },
}

Every mutation automatically uses the new ID. No code changes needed anywhere else.


Query and Mutation Type Safety

Fully Typed Query Results

const posts = useCorsairQuery(
  'all published posts with authors and comment count'
)

// posts is typed as:
// Array<{
//   id: string;
//   title: string;
//   content: string;
//   published: boolean;
//   createdAt: Date;
//   author: {
//     id: string;
//     name: string;
//     email: string;
//   };
//   commentCount: number;
// }>

What This Enables

Autocomplete when accessing fields:

posts.forEach(post => {
  console.log(post.t|)  // <- Autocomplete shows: title, createdAt
  console.log(post.author.n|)  // <- Autocomplete shows: name
});

Type errors for invalid access:

// ❌ Type error: Property 'views' does not exist
console.log(post.views)

// ❌ Type error: Property 'username' does not exist
console.log(post.author.username)

Refactoring safety:

// If you change the query to remove 'commentCount'
const posts = useCorsairQuery(
  'all published posts with authors' // <- No longer includes comment count
)

// TypeScript immediately shows errors everywhere commentCount was used
// ❌ Type error: Property 'commentCount' does not exist
console.log(post.commentCount)

Mutation Input Type Safety

Typed Mutation Parameters

const createPost = useCorsairMutation(
  'create post with title, content, and author id'
)

// TypeScript knows exactly what fields are required
await createPost.mutate({
  title: 'My Post',
  content: 'Post content',
  authorId: 'user_123',
})

What TypeScript Catches

Missing required fields:

// ❌ Type error: Property 'content' is missing
await createPost.mutate({
  title: 'My Post',
  authorId: 'user_123',
})

Wrong field types:

// ❌ Type error: Type 'number' is not assignable to type 'string'
await createPost.mutate({
  title: 'My Post',
  content: 'Post content',
  authorId: 12345, // <- Should be string
})

Extra fields that don't exist:

// ❌ Type error: Object literal may only specify known properties
await createPost.mutate({
  title: 'My Post',
  content: 'Post content',
  authorId: 'user_123',
  published: true, // <- This field doesn't exist in the mutation
})

Schema Change Propagation

The Scenario

Your User model changes from a single fullName field to separate firstName and lastName fields.

Before:

model User {
  id       String @id
  email    String @unique
  fullName String
}

After:

model User {
  id        String @id
  email     String @unique
  firstName String
  lastName  String
}

What Happens

Corsair regenerates queries automatically:

const users = useCorsairQuery('all users with names')

// Before regeneration - Type error!
// Array<{ id: string; email: string; fullName: string }>

// After regeneration - Types updated!
// Array<{ id: string; email: string; firstName: string; lastName: string }>

TypeScript shows errors everywhere the old field was used:

// ❌ Type error: Property 'fullName' does not exist
<div>{user.fullName}</div>

// ✅ Fix it
<div>{user.firstName} {user.lastName}</div>

Benefits:

  • Every breaking change is caught at compile time
  • No silent runtime failures
  • Refactor with confidence — TypeScript guides you to every place that needs updating

Type Exports for Component Props

The QueryOutputs Type

Corsair exports types for every query, making it trivial to build type-safe component interfaces.

import { QueryOutputs } from '@/corsair/types'

interface UserCardProps {
  user: QueryOutputs['get all users'][number]
}

export function UserCard({ user }: UserCardProps) {
  return (
    <div>
      <h3>
        {user.firstName} {user.lastName}
      </h3>
      <p>{user.email}</p>
    </div>
  )
}

What This Gives You

Autocomplete in components:

export function UserCard({ user }: UserCardProps) {
  return <div>{user.|}</div>  // <- Shows: id, email, firstName, lastName
}

Automatic updates when queries change:

// You update the query to include 'avatar'
const users = useCorsairQuery('all users with names and avatar')

// TypeScript immediately knows about the new field
export function UserCard({ user }: UserCardProps) {
  return (
    <div>
      <img src={user.avatar} /> // ✅ TypeScript knows this exists now
      <h3>
        {user.firstName} {user.lastName}
      </h3>
    </div>
  )
}

Type safety across your component tree:

interface UserListProps {
  users: QueryOutputs['get all users']
  onUserClick: (user: QueryOutputs['get all users'][number]) => void
}

export function UserList({ users, onUserClick }: UserListProps) {
  return (
    <div>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user} // ✅ Types match perfectly
          onClick={() => onUserClick(user)} // ✅ Types match perfectly
        />
      ))}
    </div>
  )
}

Plugin Type Safety

Typed Plugin Parameters

const sendEmail = useCorsairMutation(
  'send email via Resend with subject and HTML content'
)

// TypeScript knows the exact shape of Resend parameters
await sendEmail.mutate({
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome to our app</h1>',
})

What Gets Caught

Invalid email format:

// ❌ Type error: Type 'string' does not match pattern for email
await sendEmail.mutate({
  to: 'not-an-email',
  subject: 'Welcome!',
  html: '<h1>Welcome</h1>',
})

Missing required plugin configuration:

// If you haven't configured Resend in your config
const sendEmail = useCorsairMutation('send email via Resend')

// ❌ Type error: Plugin 'resend' is not configured

Using unavailable plugin features:

// If your Resend config doesn't include attachment support
await sendEmail.mutate({
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome</h1>',
  attachments: [...]  // ❌ Type error: Attachments not enabled in config
});

Multi-Service Type Safety

Typed Multi-Step Mutations

const onboardUser = useCorsairMutation(
  'create user, send welcome email via Resend, track signup in PostHog, and notify #general Slack channel'
)

await onboardUser.mutate({
  email: 'user@example.com',
  name: 'John Doe',
  plan: 'pro',
})

What TypeScript Ensures

All services receive correct types:

// ❌ Type error if Slack channel doesn't exist
// ❌ Type error if PostHog event properties are wrong
// ❌ Type error if Resend email fields are missing
// ✅ All services properly typed and validated

Partial failures are typed:

try {
  await onboardUser.mutate({ email, name, plan })
} catch (error) {
  // error is typed as:
  // {
  //   service: 'resend' | 'posthog' | 'slack' | 'database';
  //   message: string;
  //   originalError: unknown;
  // }

  if (error.service === 'resend') {
    // Handle email failure specifically
  }
}

The Type-Driven Development Flow

1. Define Your Intent

const users = useCorsairQuery(
  'active users who logged in within last 30 days with their order count'
)

2. Corsair Generates Types

// Type generated automatically:
// Array<{
//   id: string;
//   email: string;
//   firstName: string;
//   lastName: string;
//   lastLogin: Date;
//   orderCount: number;
// }>

3. Build UI with Full Type Safety

export function ActiveUsers() {
  const { data: users } = useCorsairQuery(
    'active users who logged in within last 30 days with their order count'
  )

  return (
    <div>
      {users?.map(user => (
        <div key={user.id}>
          <h3>
            {user.firstName} {user.lastName}
          </h3>
          <p>Last login: {user.lastLogin.toLocaleDateString()}</p>
          <p>Orders: {user.orderCount}</p>
        </div>
      ))}
    </div>
  )
}

4. Refactor with Confidence

Change the query:

const users = useCorsairQuery(
  'active users who logged in within last 30 days with their order count and total spent'
)

TypeScript immediately guides you:

export function ActiveUsers() {
  const { data: users } = useCorsairQuery(
    'active users who logged in within last 30 days with their order count and total spent'
  )

  return (
    <div>
      {users?.map(user => (
        <div key={user.id}>
          <h3>
            {user.firstName} {user.lastName}
          </h3>
          <p>Last login: {user.lastLogin.toLocaleDateString()}</p>
          <p>Orders: {user.orderCount}</p>
          <p>Total spent: ${user.totalSpent}</p> {/* ✅ New field available */}
        </div>
      ))}
    </div>
  )
}

Preventing Runtime Errors

The Traditional Problem

Vibe coded or manually written code:

// No type safety
const channel = '#genral' // ⚠️ Typo - will fail at runtime
await slack.postMessage({ channel, text: 'Hello' })

// No validation
const user = await db.user.findUnique({
  where: { id: userId },
  select: { fullName: true }, // ⚠️ Field doesn't exist - will fail at runtime
})

The Corsair Solution

Everything typed at compile time:

// ❌ Type error: '"genral"' is not assignable to '"general" | "technology" | "notifications-error"'
const sendMessage = useCorsairMutation('send slack message')
await sendMessage.mutate({
  channel: 'genral', // <- Caught before running
  message: 'Hello',
})

// ❌ Type error: Property 'fullName' does not exist
const user = useCorsairQuery('user full name by id', { userId })
console.log(user.fullName) // <- Caught before running

The Goal

Corsair's TypeScript integration provides:

  1. Compile-time error detection — catch bugs before they reach production
  2. Best-in-class developer experience — autocomplete and intellisense everywhere
  3. Configuration type safety — changes ripple through your entire codebase
  4. Schema-driven types — automatically updated when your database changes
  5. Plugin type safety — validated parameters for every third-party service
  6. Refactoring confidence — TypeScript guides you through every change

The principle: If TypeScript is happy, you can be confident your code will work. If TypeScript shows errors, something is guaranteed to be wrong—and you caught it early.

You get production reliability with an exceptional developer experience.