| name | arcanea-typescript-expert |
| description | TypeScript strict mode expert patterns for the Arcanea stack. Use when writing TypeScript types, resolving type errors, designing type-safe APIs, working with generics, discriminated unions, Zod validation, or any TypeScript-heavy work. Triggers on: TypeScript, type error, any, generic, interface, type, discriminated union, Zod, type safety, strict mode, infer. Stack: TypeScript 5.5+ strict mode, Next.js 16, Supabase typed client, Vercel AI SDK 6, Zod. |
Arcanea TypeScript Expert
"Lyria guards the Sight Gate at 639 Hz — Intuition, vision. TypeScript's type system is the Sight Gate of code: it reveals what cannot be seen at runtime."
Arcanea's TypeScript Rules
Non-negotiable:
strict: true in tsconfig — always
- No
any — use unknown and narrow it
- No type assertions (
as) without a comment explaining why
- All function params and returns typed explicitly in public APIs
- Zod for runtime validation of external data (API responses, form data, env vars)
Core Patterns
Discriminated Unions — Arcanea Domain Types
type GateStatus =
| { status: 'locked' }
| { status: 'unlocked'; unlockedAt: Date; score: number }
| { status: 'mastered'; masteredAt: Date; score: number; mastery: string }
function renderGateStatus(gate: GateStatus) {
switch (gate.status) {
case 'locked':
return 'Sealed by Malachar'
case 'unlocked':
return `Unlocked — Score: ${gate.score}`
case 'mastered':
return `Mastered — ${gate.mastery}`
}
}
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E }
async function unlockGate(userId: string, gate: GateName): Promise<Result<GateStatus>> {
try {
const data = await db.unlockGate(userId, gate)
return { success: true, data }
} catch (error) {
return { success: false, error: error instanceof Error ? error : new Error(String(error)) }
}
}
Literal Types for Arcanea Constants
const GATES = [
'foundation', 'flow', 'fire', 'heart', 'voice',
'sight', 'crown', 'shift', 'unity', 'source'
] as const
type GateName = typeof GATES[number]
const ELEMENTS = ['earth', 'water', 'fire', 'wind', 'void', 'spirit'] as const
type Element = typeof ELEMENTS[number]
const MAGIC_RANKS = ['apprentice', 'mage', 'master', 'archmage', 'luminor'] as const
type MagicRank = typeof MAGIC_RANKS[number]
const GATE_FREQUENCIES: Record<GateName, number> = {
foundation: 174, flow: 285, fire: 396, heart: 417, voice: 528,
sight: 639, crown: 741, shift: 852, unity: 963, source: 1111,
}
Branded Types — Prevent ID mixing
type UserId = string & { readonly __brand: 'UserId' }
type GuardianId = string & { readonly __brand: 'GuardianId' }
type PromptId = string & { readonly __brand: 'PromptId' }
function toUserId(id: string): UserId { return id as UserId }
function toGuardianId(id: string): GuardianId { return id as GuardianId }
async function getUserProgress(userId: UserId): Promise<GateStatus[]> { ... }
async function getGuardian(guardianId: GuardianId): Promise<Guardian> { ... }
getUserProgress(toGuardianId('abc'))
Generic Utilities
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type RequiredPick<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>
type Paginated<T> = {
data: T[]
total: number
page: number
pageSize: number
hasMore: boolean
}
type Awaited<T> = T extends Promise<infer U> ? U : T
import type { Database } from '@/types/supabase'
type GuardianRow = Database['public']['Tables']['guardians']['Row']
type GuardianInsert = Database['public']['Tables']['guardians']['Insert']
type GuardianUpdate = Database['public']['Tables']['guardians']['Update']
Zod — Runtime Validation
API Route Validation
import { z } from 'zod'
import { NextRequest, NextResponse } from 'next/server'
const UnlockGateSchema = z.object({
gate: z.enum(['foundation','flow','fire','heart','voice','sight','crown','shift','unity','source']),
score: z.number().int().min(0).max(100),
completedAt: z.string().datetime().optional(),
})
type UnlockGateInput = z.infer<typeof UnlockGateSchema>
export async function POST(req: NextRequest) {
const body = await req.json()
const parsed = UnlockGateSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Invalid request', details: parsed.error.flatten() },
{ status: 400 }
)
}
const { gate, score } = parsed.data
}
Environment Variables — Type-safe
import { z } from 'zod'
const EnvSchema = z.object({
NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
})
export const env = EnvSchema.parse(process.env)
Form Data Validation
import { z } from 'zod'
export const GateQuizAnswerSchema = z.object({
questionId: z.string().uuid(),
selectedOption: z.number().int().min(0).max(3),
timeSpent: z.number().positive(),
})
export const GateQuizSubmissionSchema = z.object({
gate: z.enum(['foundation','flow','fire','heart','voice','sight','crown','shift','unity','source']),
answers: z.array(GateQuizAnswerSchema).min(1).max(20),
startedAt: z.string().datetime(),
})
export type GateQuizSubmission = z.infer<typeof GateQuizSubmissionSchema>
Narrowing — No More any
function isGuardian(value: unknown): value is Guardian {
return (
typeof value === 'object' &&
value !== null &&
'gate' in value &&
'name' in value &&
'frequency_hz' in value
)
}
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(x)}`)
}
function getElementColor(element: Element): string {
switch (element) {
case 'earth': return '#4a7c59'
case 'water': return '#78a6ff'
case 'fire': return '#ff6b35'
case 'wind': return '#e8e8e8'
case 'void': return '#8b5cf6'
case 'spirit': return '#ffd700'
default: return assertNever(element)
}
}
React + TypeScript Patterns
Component Props with Variants
type GuardianCardProps =
| { variant: 'compact'; guardian: Pick<Guardian, 'name' | 'gate' | 'element'> }
| { variant: 'full'; guardian: Guardian; onUnlock?: () => void }
| { variant: 'loading' }
function GuardianCard(props: GuardianCardProps) {
if (props.variant === 'loading') return <GlassCardSkeleton />
if (props.variant === 'compact') return <CompactView guardian={props.guardian} />
return <FullView guardian={props.guardian} onUnlock={props.onUnlock} />
}
Server Component typed params (Next.js 16)
type PageProps = {
params: Promise<{ gate: GateName }>
searchParams: Promise<{ tab?: string }>
}
export default async function GatePage({ params, searchParams }: PageProps) {
const { gate } = await params
const { tab } = await searchParams
}
Typed event handlers
import type { ChangeEvent, FormEvent, KeyboardEvent } from 'react'
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
const data = new FormData(e.currentTarget)
}
tsconfig.json — Arcanea Baseline
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
Extra flags explained:
noUncheckedIndexedAccess: arr[0] is T | undefined, not T
exactOptionalPropertyTypes: { a?: string } means the key may not exist — not string | undefined
noImplicitReturns: all code paths in functions must return
noFallthroughCasesInSwitch: prevents missing break bugs
Quick Checklist
Before any TypeScript PR in Arcanea: