
Typescript Skills
- 79 installs
- 835 repo stars
- Updated June 10, 2026
- llama-farm/llamafarm
Helps with ai & agent building tasks.
About
typescript-skills is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- typescript-skills
- AI & Agent Building
- AI-coding skill
Typescript Skills by the numbers
- 79 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #5,292 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/llama-farm/llamafarm --skill typescript-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 79 |
|---|---|
| repo stars | ★ 835 |
| Last updated | June 10, 2026 |
| Repository | llama-farm/llamafarm ↗ |
What it does
Helps with ai & agent building tasks.
Files
TypeScript Skills for LlamaFarm
Shared TypeScript best practices for Designer (React) and Electron App subsystems.
Overview
This skill covers idiomatic TypeScript patterns for LlamaFarm's frontend applications:
- designer/: React 18 + TanStack Query + TailwindCSS + Radix UI
- electron-app/: Electron 28 + Electron Vite
Tech Stack
| Subsystem | Framework | Build | Key Libraries |
|---|---|---|---|
| designer | React 18 | Vite | TanStack Query, Radix UI, axios, react-router-dom |
| electron-app | Electron 28 | electron-vite | electron-updater, axios |
Configuration
Both projects use strict TypeScript:
{
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}Core Principles
1. Strict mode always - Never use any without explicit justification 2. Prefer interfaces - Use interface for object shapes, type for unions/intersections 3. Explicit return types - Always type public function returns 4. Immutability - Use readonly and as const where applicable 5. Null safety - Handle null/undefined explicitly, avoid non-null assertions
Related Documents
- patterns.md - Idiomatic TypeScript patterns
- typing.md - Strict typing, generics, utility types
- testing.md - Vitest and testing patterns
- security.md - XSS prevention, input validation
Quick Reference
React Component Pattern
interface Props {
readonly title: string
readonly onAction?: () => void
}
function MyComponent({ title, onAction }: Props): JSX.Element {
return <button onClick={onAction}>{title}</button>
}TanStack Query Hook Pattern
export const projectKeys = {
all: ['projects'] as const,
lists: () => [...projectKeys.all, 'list'] as const,
detail: (id: string) => [...projectKeys.all, 'detail', id] as const,
}
export function useProject(id: string) {
return useQuery({
queryKey: projectKeys.detail(id),
queryFn: () => fetchProject(id),
enabled: !!id,
})
}Error Class Pattern
export class ApiError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly response?: unknown
) {
super(message)
this.name = 'ApiError'
}
}Checklist Summary
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Typing | 3 | 4 | 2 | 1 |
| Patterns | 2 | 3 | 3 | 2 |
| Testing | 2 | 3 | 2 | 1 |
| Security | 4 | 2 | 1 | 0 |
TypeScript Patterns for LlamaFarm
Idiomatic patterns for React and Electron TypeScript code.
React Component Patterns
Functional Components with Props Interface
interface ButtonProps {
readonly label: string
readonly variant?: 'primary' | 'secondary'
readonly disabled?: boolean
readonly onClick?: () => void
}
function Button({ label, variant = 'primary', disabled, onClick }: ButtonProps): JSX.Element {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
)
}Props with Children
interface CardProps {
readonly title: string
readonly children: React.ReactNode
}
function Card({ title, children }: CardProps): JSX.Element {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
)
}Hook Patterns
Custom Hook with Return Type
interface UseToggleReturn {
isOpen: boolean
open: () => void
close: () => void
toggle: () => void
}
function useToggle(initial = false): UseToggleReturn {
const [isOpen, setIsOpen] = useState(initial)
const open = useCallback(() => setIsOpen(true), [])
const close = useCallback(() => setIsOpen(false), [])
const toggle = useCallback(() => setIsOpen(prev => !prev), [])
return { isOpen, open, close, toggle }
}Query Key Factory Pattern
export const projectKeys = {
all: ['projects'] as const,
lists: () => [...projectKeys.all, 'list'] as const,
list: (namespace: string) => [...projectKeys.lists(), namespace] as const,
details: () => [...projectKeys.all, 'detail'] as const,
detail: (namespace: string, id: string) => [...projectKeys.details(), namespace, id] as const,
}TanStack Query Hook Pattern
export function useProjects(namespace: string) {
return useQuery({
queryKey: projectKeys.list(namespace),
queryFn: () => projectService.listProjects(namespace),
enabled: !!namespace,
staleTime: 5 * 60 * 1000,
retry: 1,
refetchOnWindowFocus: false,
})
}
export function useCreateProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ namespace, request }: { namespace: string; request: CreateProjectRequest }) =>
projectService.createProject(namespace, request),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: projectKeys.list(variables.namespace) })
},
})
}API Service Patterns
Service Module with Typed Functions
export async function getProject(
namespace: string,
projectId: string
): Promise<GetProjectResponse> {
const { data } = await apiClient.get<GetProjectResponse>(
`/projects/${encodeURIComponent(namespace)}/${encodeURIComponent(projectId)}`
)
return data
}
const projectService = {
listProjects,
getProject,
createProject,
updateProject,
deleteProject,
}
export default projectServiceCustom Error Classes
export class ApiError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly response?: unknown
) {
super(message)
this.name = 'ApiError'
}
}
export class ValidationError extends Error {
constructor(
message: string,
public readonly validationErrors: unknown
) {
super(message)
this.name = 'ValidationError'
}
}Electron Patterns
Class-Based Architecture
export class WindowManager {
private mainWindow: BrowserWindow | null = null
private splashWindow: BrowserWindow | null = null
createMainWindow(): BrowserWindow {
this.mainWindow = new BrowserWindow({
width: 1400,
height: 900,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, '../preload/index.js'),
},
})
return this.mainWindow
}
cleanup(): void {
this.mainWindow?.close()
this.splashWindow?.close()
}
}IPC Handler Pattern
private setupIPCHandlers(): void {
ipcMain.handle('cli:info', async (): Promise<CLIInfo> => {
const isInstalled = await this.cliInstaller.isInstalled()
return {
isInstalled,
path: isInstalled ? this.cliInstaller.getCLIPath() : null,
}
})
}---
Checklist
PATTERNS-001: Use query key factories
- Description: Query keys should use factory pattern for consistency
- Search:
grep -r "queryKey:" designer/src/hooks/ - Pass: All queryKey values use factory functions (e.g.,
projectKeys.detail()) - Fail: Inline arrays like
queryKey: ['projects', id] - Severity: High
- Fix: Create key factory objects and use them consistently
PATTERNS-002: Avoid inline object types in function signatures
- Description: Extract inline types to named interfaces
- Search:
grep -rE "function \w+\([^)]*\{[^}]+\}[^)]*\)" designer/src/ - Pass: No matches (all params use named types)
- Fail: Functions with inline object types in parameters
- Severity: Medium
- Fix: Extract to interface above function
PATTERNS-003: Use readonly for immutable props
- Description: Props interfaces should use readonly modifier
- Search:
grep -rE "interface \w+Props" designer/src/ - Pass: Props use
readonlyfor all fields - Fail: Mutable props without readonly
- Severity: Low
- Fix: Add
readonlyto interface fields
PATTERNS-004: Prefer named exports for hooks
- Description: Custom hooks should be named exports, not default
- Search:
grep -r "export default function use" designer/src/hooks/ - Pass: No default exports for hooks
- Fail: Hooks exported as default
- Severity: Medium
- Fix: Use
export function useX()instead
PATTERNS-005: Mutation hooks should invalidate queries
- Description: useMutation hooks should invalidate related queries on success
- Search:
grep -A10 "useMutation" designer/src/hooks/ - Pass: All mutations have
onSuccesswithinvalidateQueries - Fail: Mutations without cache invalidation
- Severity: High
- Fix: Add
onSuccesshandler withqueryClient.invalidateQueries()
PATTERNS-006: Use callback refs for event handlers
- Description: Event handlers should use useCallback for stable references
- Search:
grep -rE "onClick=\{[^}]+\}" designer/src/components/ - Pass: Complex handlers wrapped in useCallback
- Fail: Inline arrow functions in JSX for handlers
- Severity: Low
- Fix: Extract to useCallback or stable function reference
PATTERNS-007: Electron classes use private fields
- Description: Class fields should use private modifier
- Search:
grep -r "class.*{" electron-app/src/ - Pass: Instance fields marked private
- Fail: Public fields without explicit access modifier
- Severity: Medium
- Fix: Add
privatemodifier to internal fields
PATTERNS-008: API functions return typed promises
- Description: API service functions must have explicit return types
- Search:
grep -rE "async function|async \(" designer/src/api/ - Pass: All async functions have
Promise<T>return type - Fail: Missing return type annotation
- Severity: High
- Fix: Add explicit
Promise<ResponseType>return annotation
PATTERNS-009: Context providers use typed value
- Description: React contexts should define value type explicitly
- Search:
grep -r "createContext" designer/src/contexts/ - Pass: Context created with typed default value
- Fail: Context with undefined or any type
- Severity: Medium
- Fix: Define context value interface and use it
PATTERNS-010: useMemo/useCallback have correct dependencies
- Description: Memoization hooks must list all dependencies
- Search:
grep -rE "useMemo|useCallback" designer/src/ - Pass: All referenced variables in dependency array
- Fail: Missing dependencies (eslint-plugin-react-hooks should catch)
- Severity: High
- Fix: Add missing dependencies or restructure code
Security Patterns for LlamaFarm TypeScript
XSS prevention, input validation, and secure coding practices.
XSS Prevention
Sanitize User Input for Display
const MAX_CONFIG_VALUE_LENGTH = 100
export const sanitizeConfigValue = (value: unknown): string => {
if (!value) return 'Not set'
const str = String(value)
.replace(/[<>'"]/g, '') // Remove HTML/script injection characters
.trim()
return str.length > MAX_CONFIG_VALUE_LENGTH
? str.substring(0, MAX_CONFIG_VALUE_LENGTH) + '...'
: str
}React's Built-in Protection
React escapes values in JSX by default:
// SAFE: React escapes the content
<div>{userInput}</div>
// DANGEROUS: Bypasses React's protection
<div dangerouslySetInnerHTML={{ __html: userInput }} />Sanitize HTML Content
When HTML must be rendered, use a sanitizer:
import rehypeSanitize from 'rehype-sanitize'
import ReactMarkdown from 'react-markdown'
// Safe markdown rendering
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>
{userContent}
</ReactMarkdown>URL Validation
Validate URLs Before Use
export const isValidAndSafeURL = (urlString: string): boolean => {
try {
const url = new URL(urlString)
// Only allow http and https protocols
if (!['http:', 'https:'].includes(url.protocol)) {
return false
}
// Warn about localhost/private IPs in production
const hostname = url.hostname.toLowerCase()
const isLocalhost =
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
if (import.meta.env.PROD && isLocalhost) {
console.warn('Localhost/private IP in production:', hostname)
}
return true
} catch {
return false
}
}Extract Safe Hostname
export const extractSafeHostname = (urlValue: unknown): string => {
if (!urlValue) return 'Not set'
const urlString = String(urlValue)
if (!isValidAndSafeURL(urlString)) {
return 'Invalid URL'
}
try {
const url = new URL(urlString)
const hostname = sanitizeConfigValue(url.hostname)
const port = url.port ? sanitizeConfigValue(url.port) : ''
return port ? `${hostname}:${port}` : hostname
} catch {
return 'Invalid URL'
}
}Navigation State Validation
Validate Router State
export const validateNavigationState = (state: unknown): {
database: string
strategyName: string
strategyType: string
currentConfig: Record<string, unknown>
isDefault: boolean
} => {
const s = state as Record<string, unknown>
// Validate database name (alphanumeric and underscores only)
const database =
typeof s?.database === 'string' && /^[a-zA-Z0-9_]+$/.test(s.database)
? s.database
: 'main_database'
// Validate strategy name
const strategyName =
typeof s?.strategyName === 'string' &&
/^[a-zA-Z0-9\s_-]+$/.test(s.strategyName)
? s.strategyName
: ''
// Validate strategy type against allowed list
const allowedTypes = ['BasicSimilarityStrategy', 'MultiQueryStrategy']
const strategyType =
typeof s?.strategyType === 'string' && allowedTypes.includes(s.strategyType)
? s.strategyType
: 'BasicSimilarityStrategy'
// Validate config is object
const currentConfig =
s?.currentConfig &&
typeof s.currentConfig === 'object' &&
!Array.isArray(s.currentConfig)
? (s.currentConfig as Record<string, unknown>)
: {}
const isDefault = typeof s?.isDefault === 'boolean' ? s.isDefault : false
return { database, strategyName, strategyType, currentConfig, isDefault }
}Input Sanitization
Sanitize Filter Keys
const MAX_FILTER_KEY_LENGTH = 50
export const sanitizeFilterKey = (key: string): string => {
return key.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, MAX_FILTER_KEY_LENGTH)
}Sanitize Filter Values
const MAX_FILTER_VALUE_LENGTH = 200
export const sanitizeFilterValue = (value: string): string => {
return value
.replace(/[<>'"\\]/g, '')
.trim()
.substring(0, MAX_FILTER_VALUE_LENGTH)
}Parse Numeric Values Safely
const parseNumericValue = (raw: string): number | null => {
const num = Number(raw)
if (Number.isNaN(num)) return null
if (!Number.isFinite(num)) return null
if (Math.abs(num) > Number.MAX_SAFE_INTEGER) return null
return num
}Reserved Names Protection
export const RESERVED_STRATEGY_NAMES = [
'default',
'null',
'undefined',
'none',
'system',
'admin',
'root',
'all',
'any',
]
export const validateStrategyName = (name: string): string | null => {
const trimmedName = name.trim()
if (!trimmedName) {
return 'Strategy name is required'
}
if (RESERVED_STRATEGY_NAMES.includes(trimmedName.toLowerCase())) {
return `"${trimmedName}" is a reserved name.`
}
if (!/^[a-zA-Z0-9_-]+$/.test(trimmedName)) {
return 'Cannot contain spaces or special characters'
}
if (trimmedName.length > 100) {
return 'Strategy name must be 100 characters or less'
}
return null
}Electron Security
Context Isolation
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // Disable Node.js in renderer
contextIsolation: true, // Isolate preload scripts
preload: path.join(__dirname, '../preload/index.js'),
},
})Safe DOM Building in Preload
// Build DOM elements safely to prevent XSS
const item = document.createElement('div')
item.className = 'model-item'
const name = document.createElement('div')
name.className = 'model-name'
name.textContent = model.display_name // textContent escapes HTML
// Sanitize status to only allow known values
const VALID_STATUSES = ['present', 'downloading', 'checking', 'error']
const safeStatus = VALID_STATUSES.includes(model.status) ? model.status : 'checking'API Security
URL Encoding for Path Parameters
const { data } = await apiClient.get(
`/projects/${encodeURIComponent(namespace)}/${encodeURIComponent(projectId)}`
)Validate Response Data
function isValidProject(data: unknown): data is Project {
return (
typeof data === 'object' &&
data !== null &&
'namespace' in data &&
typeof (data as Record<string, unknown>).namespace === 'string' &&
'name' in data &&
typeof (data as Record<string, unknown>).name === 'string'
)
}---
Checklist
SECURITY-001: No dangerouslySetInnerHTML with user input
- Description: Never use dangerouslySetInnerHTML with unsanitized content
- Search:
grep -r "dangerouslySetInnerHTML" designer/src/ - Pass: No usage, or only with sanitized/static content
- Fail: Using with user-provided data
- Severity: Critical
- Fix: Use React's default escaping or rehype-sanitize
SECURITY-002: URL encoding for path parameters
- Description: Path parameters must be URL encoded
- Search:
grep -rE "apiClient\.(get|post|put|delete)\(" designer/src/api/ - Pass: All dynamic path segments use encodeURIComponent
- Fail: Unencoded user input in URLs
- Severity: Critical
- Fix: Wrap with
encodeURIComponent()
SECURITY-003: Validate URL protocols
- Description: Only allow http/https protocols
- Search:
grep -r "new URL" designer/src/ - Pass: Protocol validated before use
- Fail: Accepting file://, javascript:, data: URLs
- Severity: Critical
- Fix: Check url.protocol against allowed list
SECURITY-004: Sanitize navigation state
- Description: Router location.state must be validated
- Search:
grep -r "location.state" designer/src/ - Pass: State validated with type guards
- Fail: Direct use of unvalidated state
- Severity: Critical
- Fix: Use validateNavigationState() or similar
SECURITY-005: Electron context isolation enabled
- Description: Electron windows must use context isolation
- Search:
grep -r "contextIsolation" electron-app/src/ - Pass: contextIsolation: true in all windows
- Fail: contextIsolation: false or missing
- Severity: High
- Fix: Set contextIsolation: true in webPreferences
SECURITY-006: Node integration disabled
- Description: Electron renderer must not have Node access
- Search:
grep -r "nodeIntegration" electron-app/src/ - Pass: nodeIntegration: false in all windows
- Fail: nodeIntegration: true
- Severity: High
- Fix: Set nodeIntegration: false, use preload for IPC
SECURITY-007: Length limits on user input
- Description: User input must have maximum length limits
- Search:
grep -r "substring\|slice\|MAX_" designer/src/utils/security.ts - Pass: All sanitization functions limit length
- Fail: Unbounded string processing
- Severity: Medium
- Fix: Add substring(0, MAX_LENGTH) to sanitizers
Testing Patterns for LlamaFarm TypeScript
Vitest testing patterns for Designer React application.
Test Configuration
Designer uses Vitest with jsdom environment:
// vitest.config.ts
export default mergeConfig(viteConfig, defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
},
}))Test Setup
// src/test/setup.ts
import { afterEach, beforeAll, afterAll, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
import { server } from './mocks/server'
beforeAll(() => {
server.listen({ onUnhandledRequest: 'warn' })
})
afterEach(() => {
cleanup()
server.resetHandlers()
})
afterAll(() => {
server.close()
})MSW Mock Server
// src/test/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)Test Factory Pattern
// src/test/factories/projectFactory.ts
import { Project, ListProjectsResponse } from '../../types/project'
interface MockProjectOptions {
namespace?: string
name?: string
config?: Record<string, unknown>
validation_error?: string | null
}
export function createMockProject(options: MockProjectOptions = {}): Project {
const {
namespace = 'default',
name = 'test-project',
config = {
version: 'v1',
name,
namespace,
runtime: { provider: 'ollama', model: 'llama3.2:3b' },
prompts: [],
},
validation_error = null,
} = options
return { namespace, name, config, validation_error }
}
export function createMockProjectsList(
namespace = 'default',
count = 2
): ListProjectsResponse {
const projects = Array.from({ length: count }, (_, i) =>
createMockProject({ namespace, name: `project-${i + 1}` })
)
return { total: projects.length, projects }
}API Service Tests
import { describe, it, expect, beforeEach } from 'vitest'
import { http, HttpResponse } from 'msw'
import { server } from '../../test/mocks/server'
import { listProjects, createProject } from '../projectService'
import { createMockProject, createMockProjectsList } from '../../test/factories/projectFactory'
const API_BASE = 'http://localhost:14345/v1'
describe('projectService', () => {
beforeEach(() => {
server.resetHandlers()
})
describe('listProjects', () => {
it('should list projects for namespace', async () => {
const mockResponse = createMockProjectsList('default', 2)
server.use(
http.get(`${API_BASE}/projects/:namespace`, () => {
return HttpResponse.json(mockResponse)
})
)
const result = await listProjects('default')
expect(result).toEqual(mockResponse)
expect(result.total).toBe(2)
})
it('should handle API errors', async () => {
server.use(
http.get(`${API_BASE}/projects/:namespace`, () => {
return HttpResponse.json({ detail: 'Server error' }, { status: 500 })
})
)
await expect(listProjects('default')).rejects.toThrow()
})
it('should handle network errors', async () => {
server.use(
http.get(`${API_BASE}/projects/:namespace`, () => {
return HttpResponse.error()
})
)
await expect(listProjects('default')).rejects.toThrow()
})
})
})Component Tests
import { describe, it, expect } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
import MyComponent from './MyComponent'
function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
return render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
{ui}
</BrowserRouter>
</QueryClientProvider>
)
}
describe('MyComponent', () => {
it('should render title', () => {
renderWithProviders(<MyComponent title="Test" />)
expect(screen.getByText('Test')).toBeInTheDocument()
})
it('should handle click events', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
renderWithProviders(<MyComponent title="Test" onClick={onClick} />)
await user.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalledTimes(1)
})
})Hook Tests
import { describe, it, expect } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useProjects } from './useProjects'
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('useProjects', () => {
it('should fetch projects', async () => {
const { result } = renderHook(() => useProjects('default'), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data?.projects).toBeDefined()
})
})Mocking Patterns
Mock Functions
const mockFn = vi.fn()
mockFn.mockReturnValue('value')
mockFn.mockResolvedValue('async value')
mockFn.mockImplementation((arg) => arg * 2)Mock Modules
vi.mock('../api/projectService', () => ({
default: {
listProjects: vi.fn().mockResolvedValue({ projects: [], total: 0 }),
getProject: vi.fn(),
},
}))Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
clear: () => { store = {} },
}
})()
Object.defineProperty(window, 'localStorage', { value: localStorageMock })---
Checklist
TESTING-001: Tests use factories for mock data
- Description: Test data should come from factory functions
- Search:
grep -r "createMock" designer/src/**/*.test.ts - Pass: Tests use factory functions for mock data
- Fail: Inline mock objects repeated across tests
- Severity: High
- Fix: Create factory function in src/test/factories/
TESTING-002: MSW for API mocking
- Description: API tests should use MSW, not axios mocks
- Search:
grep -r "vi.mock.*axios" designer/src/ - Pass: No direct axios mocks; using MSW handlers
- Fail: Mocking axios directly
- Severity: Critical
- Fix: Use MSW http handlers instead
TESTING-003: Reset handlers between tests
- Description: MSW handlers must reset in beforeEach/afterEach
- Search:
grep -r "server.resetHandlers" designer/src/ - Pass: resetHandlers called in setup or beforeEach
- Fail: Missing handler reset between tests
- Severity: Critical
- Fix: Add
server.resetHandlers()to afterEach
TESTING-004: Use userEvent over fireEvent
- Description: Prefer userEvent for realistic interactions
- Search:
grep -r "fireEvent\." designer/src/**/*.test.tsx - Pass: Using userEvent for user interactions
- Fail: Using fireEvent for clicks, typing
- Severity: Medium
- Fix: Replace with
userEvent.setup()and async methods
TESTING-005: Async assertions use waitFor
- Description: Async state changes need waitFor
- Search:
grep -r "await.*result.current" designer/src/ - Pass: Using waitFor for async hook results
- Fail: Direct assertions on async results
- Severity: High
- Fix: Wrap in
await waitFor(() => expect(...))
TESTING-006: QueryClient in test wrappers
- Description: Query tests need fresh QueryClient
- Search:
grep -r "renderHook" designer/src/hooks/ - Pass: Each test uses new QueryClient
- Fail: Shared QueryClient across tests
- Severity: High
- Fix: Create wrapper function that instantiates new QueryClient
TESTING-007: Test error states
- Description: Tests should cover error scenarios
- Search:
grep -r "rejects.toThrow\|status: 500\|HttpResponse.error" designer/src/ - Pass: API tests include error cases
- Fail: Only happy path tested
- Severity: Medium
- Fix: Add tests for 400, 404, 500, network errors
TESTING-008: Cleanup after each test
- Description: DOM cleanup must run after each test
- Search:
grep -r "cleanup" designer/src/test/setup.ts - Pass: cleanup() called in afterEach
- Fail: Missing cleanup call
- Severity: Medium
- Fix: Add
cleanup()to afterEach in setup.ts
TypeScript Typing Guidelines
Strict typing, generics, and utility types for LlamaFarm.
Strict Mode Requirements
Both designer/ and electron-app/ use strict TypeScript:
{
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}Type vs Interface
Use interface for:
- Object shapes (props, API responses, entities)
- Extendable contracts
- Declaration merging needs
interface Project {
namespace: string
name: string
config: Record<string, unknown>
validation_error?: string | null
}
interface CreateProjectRequest {
name: string
config_template?: string
}Use type for:
- Union types
- Intersection types
- Mapped types
- Function types
type MessageRole = 'system' | 'user' | 'assistant' | 'tool'
type HealthStatus = 'healthy' | 'degraded' | 'unhealthy'
type StreamHandler = (chunk: ChatStreamChunk) => void
type ProjectWithMeta = Project & { meta: ProjectMeta }Generic Patterns
Generic API Response
interface ApiResponse<T> {
data: T
status: number
message?: string
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await apiClient.get<T>(url)
return { data: response.data, status: response.status }
}Generic Hook
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
})
const setValue = (value: T) => {
setStoredValue(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [storedValue, setValue]
}Constrained Generics
interface HasId {
id: string
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id)
}Utility Types
Common Patterns
// Partial for optional updates
interface UpdateProjectRequest {
config: Partial<ProjectConfig>
}
// Pick for subset of properties
type ProjectSummary = Pick<Project, 'namespace' | 'name'>
// Omit for excluding properties
type CreateProject = Omit<Project, 'id' | 'createdAt'>
// Record for dictionaries
type ConfigMap = Record<string, unknown>
// Required to make optional fields required
type RequiredProject = Required<Project>
// Readonly for immutable data
type ImmutableProject = Readonly<Project>Custom Utility Types
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
// Make specific properties required
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
// Extract non-nullable type
type NonNullableFields<T> = {
[K in keyof T]: NonNullable<T[K]>
}Const Assertions
Query Keys
export const projectKeys = {
all: ['projects'] as const,
lists: () => [...projectKeys.all, 'list'] as const,
detail: (id: string) => [...projectKeys.all, 'detail', id] as const,
}
// Type: readonly ['projects', 'detail', string]Configuration Objects
const ALLOWED_TYPES = ['OllamaEmbedder', 'OpenAIEmbedder', 'HuggingFaceEmbedder'] as const
type EmbedderType = typeof ALLOWED_TYPES[number]
// Type: 'OllamaEmbedder' | 'OpenAIEmbedder' | 'HuggingFaceEmbedder'Discriminated Unions
API Response States
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function handleState<T>(state: ApiState<T>): void {
switch (state.status) {
case 'idle':
// No data yet
break
case 'loading':
// Show spinner
break
case 'success':
// state.data is T here
console.log(state.data)
break
case 'error':
// state.error is Error here
console.error(state.error.message)
break
}
}Message Types
interface BaseMessage {
content: string
}
interface UserMessage extends BaseMessage {
role: 'user'
}
interface AssistantMessage extends BaseMessage {
role: 'assistant'
tool_calls?: ToolCall[]
}
interface ToolMessage extends BaseMessage {
role: 'tool'
tool_call_id: string
}
type ChatMessage = UserMessage | AssistantMessage | ToolMessageNull Safety
Optional Chaining
const hostname = config?.server?.hostname ?? 'localhost'
const port = project?.config?.runtime?.portType Guards
function isProject(value: unknown): value is Project {
return (
typeof value === 'object' &&
value !== null &&
'namespace' in value &&
'name' in value &&
'config' in value
)
}
function assertProject(value: unknown): asserts value is Project {
if (!isProject(value)) {
throw new Error('Invalid project object')
}
}Avoiding Non-Null Assertions
// BAD: Using non-null assertion
const name = project!.name
// GOOD: Explicit null check
if (!project) {
throw new Error('Project is required')
}
const name = project.name
// GOOD: Optional chaining with default
const name = project?.name ?? 'Unnamed'---
Checklist
TYPING-001: No implicit any
- Description: All variables and parameters must have explicit or inferred types
- Search:
grep -rn ": any" designer/src/ electron-app/src/ - Pass: No
anytypes, or justified with comment - Fail: Unexplained
anyusage - Severity: Critical
- Fix: Replace with specific type or
unknown
TYPING-002: No non-null assertions
- Description: Avoid
!operator; use proper null checks - Search:
grep -rE "\w+!" designer/src/ | grep -v "\.test\." | grep -v node_modules - Pass: No non-null assertions in production code
- Fail: Using
value!instead of null checks - Severity: Critical
- Fix: Add null check or use optional chaining
TYPING-003: Explicit function return types
- Description: Public functions should have explicit return types
- Search:
grep -rE "export (async )?function \w+\([^)]*\)[^:]" designer/src/ - Pass: All exported functions have return type
- Fail: Missing return type on exported function
- Severity: High
- Fix: Add explicit return type annotation
TYPING-004: Use unknown instead of any for external data
- Description: Data from external sources should be
unknown, notany - Search:
grep -rE "as any|: any" designer/src/api/ - Pass: API responses typed as specific types or unknown
- Fail: Using
anyfor API responses - Severity: High
- Fix: Define proper response interface
TYPING-005: Const assertions for literal types
- Description: Use
as constfor objects that should be readonly literals - Search:
grep -r "queryKey:" designer/src/hooks/ - Pass: Query key arrays use
as const - Fail: Missing
as conston literal arrays - Severity: Medium
- Fix: Add
as constto readonly arrays/objects
TYPING-006: Interfaces for object shapes
- Description: Object types should use interface, not inline types
- Search:
grep -rE ":\s*\{[^}]+\}" designer/src/types/ - Pass: All object shapes are named interfaces
- Fail: Inline object types in type definitions
- Severity: Medium
- Fix: Extract to named interface
TYPING-007: Discriminated unions for state
- Description: Use discriminated unions for complex state
- Search:
grep -r "type.*State" designer/src/types/ - Pass: State types use discriminated unions where appropriate
- Fail: Optional properties instead of unions
- Severity: Medium
- Fix: Refactor to discriminated union with status field
TYPING-008: Readonly for immutable data
- Description: Props and constants should use readonly
- Search:
grep -rE "interface \w+Props" designer/src/ - Pass: Props interfaces use readonly modifier
- Fail: Mutable props without readonly
- Severity: Low
- Fix: Add
readonlyto interface fields
TYPING-009: Proper generic constraints
- Description: Generics should have appropriate constraints
- Search:
grep -rE "<T>" designer/src/ - Pass: Generics constrained where needed (e.g.,
T extends object) - Fail: Unconstrained generics that should be limited
- Severity: High
- Fix: Add
extendsconstraint to generic
TYPING-010: Type guards for runtime checks
- Description: Use type guards for runtime type checking
- Search:
grep -r "is Project\|is Error\|is Array" designer/src/ - Pass: Type guards defined for complex type checks
- Fail: Type assertions without validation
- Severity: Critical
- Fix: Create type guard function with runtime checks