Instruction file imported from jiratouchmhp/Personal-Finance-Tracker (
.github/instructions/typescript-types.instructions.md). Copyright stays with the author.
TypeScript Type Standards
Maintain strict type safety throughout the codebase.
Type Definitions
Core Types Location
All shared types are defined in src/lib/types.ts:
export interface Expense {
id: string
amount: number
category: string
description: string
date: string // ISO 8601 string
}
export interface Budget {
category: string
amount: number
}
export interface Category {
id: string
name: string
icon: string
color: string
}
Import Pattern
// Always use type imports for interfaces
import type { Expense, Budget, Category } from '@/lib/types'
// For values
import { categories } from '@/lib/categories'
Component Props
Interface Naming
// Pattern: {ComponentName}Props
interface ExpenseCardProps {
expense: Expense
onEdit: () => void
onDelete: () => void
}
Optional vs Required
interface FormProps {
// Required
onSubmit: (data: Omit<Expense, 'id'>) => void
open: boolean
onOpenChange: (open: boolean) => void
// Optional (for edit mode)
expense?: Expense
// Optional with default
variant?: 'default' | 'outline'
}
Children
interface LayoutProps {
children: React.ReactNode
title?: string
}
Function Signatures
Event Handlers
const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
// Logic
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault()
// Logic
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setValue(e.target.value)
}
Callbacks
// Prefer explicit types over inference
type OnSubmitCallback = (data: Omit<Expense, 'id'>) => void
type OnDeleteCallback = (id: string) => void
interface Props {
onSubmit: OnSubmitCallback
onDelete: OnDeleteCallback
}
State Types
useState
// Explicit typing when initial value doesn't infer correctly
const [expenses, setExpenses] = useState<Expense[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
// Can infer when obvious
const [count, setCount] = useState(0) // inferred as number
const [name, setName] = useState('') // inferred as string
useKV (Spark Hook)
import { useKV } from '@github/spark'
// Always type the storage value
const [expenses, setExpenses] = useKV<Expense[]>('expenses', [])
const [budgets, setBudgets] = useKV<Budget[]>('budgets', [])
// Handle potential null/undefined
const items = expenses || []
Utility Types
Omit for Form Data
// When creating new items (ID generated by parent)
type ExpenseFormData = Omit<Expense, 'id'>
function handleSubmit(data: ExpenseFormData) {
const newExpense: Expense = {
...data,
id: generateId(),
}
}
Pick for Partial Updates
type ExpenseUpdate = Pick<Expense, 'amount' | 'description'>
function updateExpense(id: string, update: ExpenseUpdate) {
// Only amount and description can be updated
}
Partial for Optional Updates
type ExpensePartialUpdate = Partial<Expense>
function patchExpense(id: string, update: ExpensePartialUpdate) {
// Any field can be updated
}
Array Types
// Prefer array syntax
expenses: Expense[]
budgets: Budget[]
// Not Array<Expense> (less readable)
Union Types
type Status = 'idle' | 'loading' | 'success' | 'error'
type Variant = 'default' | 'outline' | 'ghost'
interface ComponentProps {
status: Status
variant?: Variant
}
Object Types
Index Signatures
// For dynamic category spending
interface CategorySpending {
[categoryId: string]: number
}
const spending: CategorySpending = {
groceries: 450.50,
dining: 234.00,
}
Record Type
// Alternative to index signature
type CategorySpending = Record<string, number>
// With specific keys
type MonthlyData = Record<'jan' | 'feb' | 'mar', number>
Type Assertions
Use sparingly and safely
// When you know more than TypeScript
const Icon = Icons[iconName as keyof typeof Icons]
// Avoid `as any` - find proper type instead
// ❌ const data = response as any
// ✅ const data = response as ExpenseData
Type Guards
function isExpense(item: unknown): item is Expense {
return (
typeof item === 'object' &&
item !== null &&
'id' in item &&
'amount' in item &&
'category' in item &&
'description' in item &&
'date' in item
)
}
// Usage
if (isExpense(data)) {
// TypeScript knows data is Expense here
console.log(data.amount)
}
Async/Promise Types
async function fetchExpenses(): Promise<Expense[]> {
const response = await fetch('/api/expenses')
const data: Expense[] = await response.json()
return data
}
// With error handling
async function saveExpense(expense: Expense): Promise<void> {
try {
await fetch('/api/expenses', {
method: 'POST',
body: JSON.stringify(expense),
})
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to save: ${error.message}`)
}
throw error
}
}
Generic Types
// For reusable components
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<div>
{items.map(item => (
<div key={keyExtractor(item)}>
{renderItem(item)}
</div>
))}
</div>
)
}
// Usage
<List<Expense>
items={expenses}
renderItem={expense => <ExpenseCard expense={expense} />}
keyExtractor={expense => expense.id}
/>
Type Safety Rules
✅ Do
- Define all props interfaces
- Use
typeimports for better tree-shaking - Type all function parameters and return values
- Use discriminated unions for state machines
- Enable strict mode in tsconfig.json
- Use
unknowninstead ofany - Type event handlers explicitly
❌ Don't
- Use
any(useunknownand narrow with type guards) - Leave function return types implicit on public APIs
- Use
@ts-ignorewithout a comment explaining why - Use
as anyto bypass type checking - Define types inline when they're reused
- Mix interfaces and types inconsistently
Validation Types
// Runtime validation with type safety
function validateExpense(data: unknown): Expense {
if (!isExpense(data)) {
throw new Error('Invalid expense data')
}
if (data.amount <= 0) {
throw new Error('Amount must be positive')
}
return data
}
tsconfig Settings
Ensure these are enabled:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Error Handling Types
// Type-safe error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E }
function parseExpense(json: string): Result<Expense> {
try {
const data = JSON.parse(json)
if (!isExpense(data)) {
return { success: false, error: new Error('Invalid expense') }
}
return { success: true, data }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
}
}
}
// Usage
const result = parseExpense(jsonString)
if (result.success) {
console.log(result.data.amount)
} else {
console.error(result.error.message)
}
Const Assertions
// For literal types
const BUDGET_STATES = {
UNDER: 'under',
NEAR: 'near',
OVER: 'over',
} as const
type BudgetState = typeof BUDGET_STATES[keyof typeof BUDGET_STATES]
// Type: 'under' | 'near' | 'over'
Type Safety Checklist
Before committing code, ensure:
- All component props have interfaces
- No
anytypes (useunknowninstead) - Event handlers are properly typed
- State variables have explicit types where needed
- Function return types are specified for public APIs
- null/undefined is handled (especially with
useKV) - Type guards used for unknown data
- No TypeScript errors in build