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 configUsing 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 configuredUsing 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 validatedPartial 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 runningThe Goal
Corsair's TypeScript integration provides:
- Compile-time error detection — catch bugs before they reach production
- Best-in-class developer experience — autocomplete and intellisense everywhere
- Configuration type safety — changes ripple through your entire codebase
- Schema-driven types — automatically updated when your database changes
- Plugin type safety — validated parameters for every third-party service
- 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.