TypeScript Best Practices for do.dev Stack
This document outlines TypeScript best practices learned from deployment issues and development experience.
Core Principles
1. Always Use Explicit Interfaces
❌ Avoid Complex Conditional Types
// This confuses TypeScript inference
const [editingDomain, setEditingDomain] = useState<typeof domains extends Array<infer T> ? T | null : null>(null)✅ Define Explicit Interfaces
// Based on Convex schema
interface Domain {
_id: Id<"loc_domains">
locationId: Id<"loc_locations">
domain: string
description?: string
dnsServer?: string
isActive?: boolean
createdBy: Id<"users">
createdAt: number
updatedAt: number
}
const [editingDomain, setEditingDomain] = useState<Domain | null>(null)2. Define Interfaces from Convex Schema
Always create TypeScript interfaces that match your Convex schema definitions:
// From schema.ts:
// loc_domains: defineTable({
// locationId: v.id("loc_locations"),
// domain: v.string(),
// description: v.optional(v.string()),
// ...
// })
// Create matching interface:
interface Domain {
_id: Id<"loc_domains">
locationId: Id<"loc_locations">
domain: string
description?: string // v.optional() becomes ?
// ... other fields
}3. Avoid any Types
❌ Don't Use any
const handleClick = (item: any) => {
console.log(item._id) // No type safety
}
domains.map((domain: any) => domain.name)✅ Use Proper Types
const handleClick = (item: Domain) => {
console.log(item._id) // Type safe
}
domains.map((domain: Domain) => domain.name)4. Local TypeScript Testing
Always test TypeScript compilation before deploying:
# Test specific app
npx tsc --noEmit --project apps/[app-name]/tsconfig.json
# Test entire project
npx tsc --noEmitCommon Patterns
1. Query Result Typing
// Convex query results
const domains = useQuery(api.loc_domains.listByLocation, {...})
// Type the results properly
const selectedDomain = domains?.find((d: Domain) => d._id === selectedDomainId)2. Form State Management
interface DomainFormData {
domain: string
description: string
dnsServer: string
isActive: boolean
}
const [domainForm, setDomainForm] = useState<DomainFormData>({
domain: "",
description: "",
dnsServer: "10.3.3.3",
isActive: true,
})3. Event Handlers
// Type event handlers properly
const handleEdit = (domain: Domain) => {
setEditingDomain(domain)
// ...
}
// In JSX
{domains.map((domain: Domain) => (
<div key={domain._id} onClick={() => handleEdit(domain)}>
{domain.domain}
</div>
))}4. Safe Array Access
// When you've verified array length
if (domains && domains.length > 0) {
setSelectedDomainId(domains[0]!._id) // Non-null assertion is safe here
}
// Alternative: optional chaining
const firstDomain = domains?.[0]
if (firstDomain) {
setSelectedDomainId(firstDomain._id)
}Deployment Considerations
Vercel TypeScript Checks
Vercel runs strict TypeScript compilation during build. Common failures:
-
Conditional type inference failures
- Solution: Use explicit interfaces
-
anytype issues- Solution: Define proper interfaces
-
Undefined object access
- Solution: Use optional chaining or non-null assertions when safe
Error Examples and Fixes
Error: Property '_id' does not exist on type 'never'
// Problem: Complex conditional type
typeof domains extends Array<infer T> ? T | null : null
// Solution: Explicit interface
Domain | nullError: Object is possibly 'undefined'
// Problem: Array access without verification
domains[0]._id
// Solution: Verify length first
if (domains && domains.length > 0) {
domains[0]!._id // Safe non-null assertion
}Convex-Specific Patterns
1. Mutation Parameters
// Type mutation parameters
const updateDomain = useMutation(api.loc_domains.update)
const handleUpdate = async (domain: Domain) => {
await updateDomain({
id: domain._id,
domain: formData.domain,
isActive: formData.isActive,
})
}2. Query Filters
// When using filter instead of withIndex
const existing = await ctx.db
.query("users")
.filter(q => q.eq(q.field("custId"), custId))
.first()Testing Strategy
- Local compilation: Always test before pushing
- Incremental fixes: Fix all TypeScript errors in one comprehensive commit
- Interface-first approach: Define interfaces before implementing components
- Schema synchronization: Keep interfaces in sync with Convex schema changes
Summary
- Define explicit interfaces based on Convex schema
- Avoid complex conditional types that confuse TypeScript
- Replace all
anytypes with proper interfaces - Test TypeScript compilation locally before deploying
- Use non-null assertions only when safe (after length checks)
- Keep interfaces synchronized with schema changes