| name | programming-typescript |
| user-invocable | false |
| description | Internal skill invoked by /programming chain. Use when writing, reviewing, or refactoring TypeScript, React, or Next.js code — enforcing type safety, fixing immutability violations, designing API routes with Zod validation, or structuring React components and hooks. Trigger keywords: TypeScript, tsx, jsx, React, Next.js, Node, Zod, useState, useEffect, useMemo, Promise.all, interface, type. |
TypeScript Patterns
Type safety eliminates runtime errors; immutability prevents state bugs. Everything else follows.
Idiomatic TypeScript patterns for React/Next.js applications with Node.js backends.
When to Use
- Writing new TypeScript code (components, hooks, API routes, utilities)
- Reviewing TypeScript/React code for type safety and correctness
- Designing API endpoints with input validation
- Refactoring JavaScript to TypeScript
When NOT to Use
- Non-TypeScript projects (Python, Swift, etc.)
- Pure Node.js without TypeScript
- Vanilla JavaScript that won't be migrated
Type Safety
Use precise types — never any
interface Market {
id: string
name: string
status: 'active' | 'resolved' | 'closed'
created_at: Date
}
function getMarket(id: string): Promise<Market> { ... }
function getMarket(id: any): Promise<any> { ... }
Exhaustive checks with never
function handleStatus(status: Market['status']): string {
switch (status) {
case 'active': return 'In progress'
case 'resolved': return 'Complete'
case 'closed': return 'Closed'
default: {
const _exhaustive: never = status
throw new Error(`Unhandled status: ${_exhaustive}`)
}
}
}
Immutability
const updatedUser = { ...user, name: 'New Name' }
const updatedArray = [...items, newItem]
const filtered = items.filter(x => x.id !== removeId)
user.name = 'New Name'
items.push(newItem)
items.splice(index, 1)
Use as const for literal types and readonly for arrays/objects that shouldn't change.
Error Handling
async function fetchData(url: string) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
console.error('Fetch failed:', error)
throw new Error(`Failed to fetch from ${url}`)
}
}
async function fetchData(url: string) {
const response = await fetch(url)
return response.json()
}
Async/Await
const [users, markets, stats] = await Promise.all([
fetchUsers(),
fetchMarkets(),
fetchStats()
])
const users = await fetchUsers()
const markets = await fetchMarkets()
const stats = await fetchStats()
React Components
Structure
interface ButtonProps {
children: React.ReactNode
onClick: () => void
disabled?: boolean
variant?: 'primary' | 'secondary'
}
export function Button({
children,
onClick,
disabled = false,
variant = 'primary'
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children}
</button>
)
}
Hooks
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
State updates
const [count, setCount] = useState(0)
setCount(prev => prev + 1)
setCount(count + 1)
Conditional rendering
{isLoading && <Spinner />}
{error && <ErrorMessage error={error} />}
{data && <DataDisplay data={data} />}
{isLoading ? <Spinner /> : error ? <ErrorMessage /> : data ? <DataDisplay /> : null}
Memoization
const sortedMarkets = useMemo(
() => markets.sort((a, b) => b.volume - a.volume),
[markets]
)
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
API Design
REST conventions
GET /api/markets # List
GET /api/markets/:id # Get one
POST /api/markets # Create
PUT /api/markets/:id # Full update
PATCH /api/markets/:id # Partial update
DELETE /api/markets/:id # Delete
GET /api/markets?status=active&limit=10&offset=0 # Filter
Response format
interface ApiResponse<T> {
success: boolean
data?: T
error?: string
meta?: { total: number; page: number; limit: number }
}
return NextResponse.json({
success: true,
data: markets,
meta: { total: 100, page: 1, limit: 10 }
})
return NextResponse.json(
{ success: false, error: 'Invalid request' },
{ status: 400 }
)
Input validation with Zod
import { z } from 'zod'
const CreateMarketSchema = z.object({
name: z.string().min(1).max(200),
description: z.string().min(1).max(2000),
endDate: z.string().datetime(),
categories: z.array(z.string()).min(1)
})
export async function POST(request: Request) {
const body = await request.json()
try {
const validated = CreateMarketSchema.parse(body)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: 'Validation failed', : error. },
{ : }
)
}
}
}
File Organization
src/
├── app/ # Next.js App Router
│ ├── api/ # API routes
│ └── (auth)/ # Route groups
├── components/
│ ├── ui/ # Generic UI (Button, Modal, etc.)
│ ├── forms/ # Form components
│ └── layouts/ # Layout components
├── hooks/ # Custom React hooks
├── lib/ # Utilities, API clients, constants
├── types/ # TypeScript type definitions
└── styles/ # Global styles
Naming: PascalCase for components (Button.tsx), camelCase with use prefix for hooks (useAuth.ts), camelCase for utilities (formatDate.ts).
Testing
test('returns empty array when no markets match query', () => {
const vector1 = [1, 0, 0]
const vector2 = [0, 1, 0]
const similarity = calculateCosineSimilarity(vector1, vector2)
expect(similarity).toBe(0)
})
Test names: describe the scenario and expected outcome, not just "works" or "test search".
Common Mistakes
| Mistake | Fix |
|---|
Using any to silence errors | Define proper types or use unknown |
| Mutating state/props directly | Use spread operator or structuredClone |
| Sequential awaits for independent calls | Use Promise.all |
useEffect with missing deps | Include all referenced values in dependency array |
Returning T | undefined without checking | Use narrowing, optional chaining, or nullish coalescing |
console.log left in production | Use a logger with levels, strip in builds |
| Functions > 50 lines | Extract sub-functions with descriptive names |
| Deep nesting (5+ levels) | Use early returns / guard clauses |