Agent skill
typescript-expert
Install this agent skill to your Project
npx add-skill https://github.com/frankxai/arcanea/tree/main/.claude/skills/development/typescript-expert
SKILL.md
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: truein tsconfig — always- No
any— useunknownand 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
// The canonical pattern for Gate states
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}` // score is available
case 'mastered':
return `Mastered — ${gate.mastery}` // mastery is available
}
}
// Result types (no thrown errors in async functions)
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
// Use const assertions for canonical data
const GATES = [
'foundation', 'flow', 'fire', 'heart', 'voice',
'sight', 'crown', 'shift', 'unity', 'source'
] as const
type GateName = typeof GATES[number]
// GateName = 'foundation' | 'flow' | 'fire' | ...
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
// Prevent passing userId where gateId is expected
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 }
// Functions are now type-safe against ID mixing
async function getUserProgress(userId: UserId): Promise<GateStatus[]> { ... }
async function getGuardian(guardianId: GuardianId): Promise<Guardian> { ... }
// This would be a compile error:
getUserProgress(toGuardianId('abc')) // Error: GuardianId is not UserId
Generic Utilities
// DeepPartial for nested update types
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
// Pick with required — make subset of fields required
type RequiredPick<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>
// Paginated response type
type Paginated<T> = {
data: T[]
total: number
page: number
pageSize: number
hasMore: boolean
}
// Async function return type extraction
type Awaited<T> = T extends Promise<infer U> ? U : T
// Extract Supabase row type
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
// app/api/unlock-gate/route.ts
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 // fully typed
// ...
}
Environment Variables — Type-safe
// lib/env.ts — validate at startup, not at use
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)
// env.NEXT_PUBLIC_SUPABASE_URL is always a valid URL string
Form Data Validation
// lib/validations/gate-quiz.ts
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(), // seconds
})
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
// Type guard pattern
function isGuardian(value: unknown): value is Guardian {
return (
typeof value === 'object' &&
value !== null &&
'gate' in value &&
'name' in value &&
'frequency_hz' in value
)
}
// Exhaustive checks
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) // compile error if new element added
}
}
React + TypeScript Patterns
Component Props with Variants
// Discriminated union props for flexible components
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)
// Next.js 16: params are Promises
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'
// Explicit event types — no implicit any
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]isT | undefined, notTexactOptionalPropertyTypes:{ a?: string }means the key may not exist — notstring | undefinednoImplicitReturns: all code paths in functions must returnnoFallthroughCasesInSwitch: prevents missingbreakbugs
Quick Checklist
Before any TypeScript PR in Arcanea:
- No
any— useunknown+ narrowing or proper types - No
aswithout a comment justifying the assertion - External data (API, form, env) validated with Zod
- Discriminated unions for state machines (gate status, auth state)
-
const GATES = [...] as constfor canonical enum-like arrays - Supabase typed client used (Database generic)
- Next.js 16 params typed as
Promise<{ ... }> -
assertNeveron switch default for exhaustive checks
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
luminor-personality-design
Design consistent, memorable AI personalities for Arcanea Luminors. From voice patterns to system prompts, create AI companions that feel magical and alive.
guardian-evolution-system
Design and implement the Guardian AI companion evolution system - from Level 1 Spark to Level 50 Transcendent. XP mechanics, personality adaptation, and player progression.
arcanea-prompt-craft
Master the Arcanean Prompt Language - advanced prompt engineering using mythological frameworks, constraint architecture, and the Centaur Principle for human-AI co-creation
Arcanea Lore Master
Maintains consistency in Arcanea world-building, including academy systems, magical mechanics, character lore, and narrative coherence across the fantasy multiverse platform
Arcanea Creator Academy
The integration of Teacher Team with Arcanea's creator education mission
Arcanea Canon Guardian
Canon consistency enforcement for Arcanea universe - tracks facts, prevents contradictions, maintains timeline, ensures lore integrity
Didn't find tool you were looking for?