# TypeScript Common Patterns Reference This document contains commonly used TypeScript patterns and idioms from real-world applications. ## React Patterns ### Component Props ```typescript // Basic props with children interface ButtonProps { variant?: 'primary' | 'secondary' | 'tertiary' size?: 'sm' | 'md' | 'lg' disabled?: boolean onClick?: () => void children: React.ReactNode } export function Button({ variant = 'primary', size = 'md', disabled = false, onClick, children, }: ButtonProps) { return ( ) } // Props extending HTML attributes interface InputProps extends React.InputHTMLAttributes { label?: string error?: string } export function Input({ label, error, ...inputProps }: InputProps) { return (
{label && } {error && {error}}
) } // Generic component props interface ListProps { items: T[] renderItem: (item: T) => React.ReactNode keyExtractor: (item: T) => string } export function List({ items, renderItem, keyExtractor }: ListProps) { return (
    {items.map((item) => (
  • {renderItem(item)}
  • ))}
) } ``` ### Hooks ```typescript // Custom hook with return type function useLocalStorage(key: string, initialValue: T): [T, (value: T) => void] { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key) return item ? JSON.parse(item) : initialValue } catch (error) { return initialValue } }) const setValue = (value: T) => { setStoredValue(value) window.localStorage.setItem(key, JSON.stringify(value)) } return [storedValue, setValue] } // Hook with options object interface UseFetchOptions { initialData?: T onSuccess?: (data: T) => void onError?: (error: Error) => void } function useFetch(url: string, options?: UseFetchOptions) { const [data, setData] = useState(options?.initialData) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { let isCancelled = false const fetchData = async () => { setLoading(true) try { const response = await fetch(url) const json = await response.json() if (!isCancelled) { setData(json) options?.onSuccess?.(json) } } catch (err) { if (!isCancelled) { const error = err instanceof Error ? err : new Error(String(err)) setError(error) options?.onError?.(error) } } finally { if (!isCancelled) { setLoading(false) } } } fetchData() return () => { isCancelled = true } }, [url]) return { data, loading, error } } ``` ### Context ```typescript // Type-safe context interface AuthContextType { user: User | null login: (email: string, password: string) => Promise logout: () => void isAuthenticated: boolean } const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null) const login = async (email: string, password: string) => { // Login logic const user = await api.login(email, password) setUser(user) } const logout = () => { setUser(null) } const value: AuthContextType = { user, login, logout, isAuthenticated: user !== null, } return {children} } // Custom hook with proper error handling export function useAuth(): AuthContextType { const context = useContext(AuthContext) if (context === undefined) { throw new Error('useAuth must be used within AuthProvider') } return context } ``` ## API Response Patterns ### Result Type Pattern ```typescript // Discriminated union for API responses type Result = | { success: true; data: T } | { success: false; error: E } // Helper functions function success(data: T): Result { return { success: true, data } } function failure(error: E): Result { return { success: false, error } } // Usage async function fetchUser(id: string): Promise> { try { const response = await fetch(`/api/users/${id}`) if (!response.ok) { return failure(new Error(`HTTP ${response.status}`)) } const data = await response.json() return success(data) } catch (error) { return failure(error instanceof Error ? error : new Error(String(error))) } } // Consuming the result const result = await fetchUser('123') if (result.success) { console.log(result.data.name) // Type-safe access } else { console.error(result.error.message) // Type-safe error handling } ``` ### Option Type Pattern ```typescript // Option/Maybe type for nullable values type Option = Some | None interface Some { readonly _tag: 'Some' readonly value: T } interface None { readonly _tag: 'None' } // Constructors function some(value: T): Option { return { _tag: 'Some', value } } function none(): Option { return { _tag: 'None' } } // Helper functions function isSome(option: Option): option is Some { return option._tag === 'Some' } function isNone(option: Option): option is None { return option._tag === 'None' } function map(option: Option, fn: (value: T) => U): Option { return isSome(option) ? some(fn(option.value)) : none() } function getOrElse(option: Option, defaultValue: T): T { return isSome(option) ? option.value : defaultValue } // Usage function findUser(id: string): Option { const user = users.find((u) => u.id === id) return user ? some(user) : none() } const user = findUser('123') const userName = getOrElse(map(user, (u) => u.name), 'Unknown') ``` ## State Management Patterns ### Discriminated Union for State ```typescript // State machine using discriminated unions type FetchState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error } // Reducer pattern type FetchAction = | { type: 'FETCH_START' } | { type: 'FETCH_SUCCESS'; payload: T } | { type: 'FETCH_ERROR'; error: Error } | { type: 'RESET' } function fetchReducer(state: FetchState, action: FetchAction): FetchState { switch (action.type) { case 'FETCH_START': return { status: 'loading' } case 'FETCH_SUCCESS': return { status: 'success', data: action.payload } case 'FETCH_ERROR': return { status: 'error', error: action.error } case 'RESET': return { status: 'idle' } } } // Usage in component function UserProfile({ userId }: { userId: string }) { const [state, dispatch] = useReducer(fetchReducer, { status: 'idle' }) useEffect(() => { dispatch({ type: 'FETCH_START' }) fetchUser(userId) .then((user) => dispatch({ type: 'FETCH_SUCCESS', payload: user })) .catch((error) => dispatch({ type: 'FETCH_ERROR', error })) }, [userId]) switch (state.status) { case 'idle': return
Ready to load
case 'loading': return
Loading...
case 'success': return
{state.data.name}
case 'error': return
Error: {state.error.message}
} } ``` ### Store Pattern ```typescript // Type-safe store implementation interface Store { getState: () => T setState: (partial: Partial) => void subscribe: (listener: (state: T) => void) => () => void } function createStore(initialState: T): Store { let state = initialState const listeners = new Set<(state: T) => void>() return { getState: () => state, setState: (partial) => { state = { ...state, ...partial } listeners.forEach((listener) => listener(state)) }, subscribe: (listener) => { listeners.add(listener) return () => listeners.delete(listener) }, } } // Usage interface AppState { user: User | null theme: 'light' | 'dark' } const store = createStore({ user: null, theme: 'light', }) // React hook integration function useStore(store: Store, selector: (state: T) => U): U { const [value, setValue] = useState(() => selector(store.getState())) useEffect(() => { const unsubscribe = store.subscribe((state) => { setValue(selector(state)) }) return unsubscribe }, [store, selector]) return value } // Usage in component function ThemeToggle() { const theme = useStore(store, (state) => state.theme) return ( ) } ``` ## Form Patterns ### Form State Management ```typescript // Generic form state interface FormState { values: T errors: Partial> touched: Partial> isSubmitting: boolean } // Form hook function useForm>( initialValues: T, validate: (values: T) => Partial>, ) { const [state, setState] = useState>({ values: initialValues, errors: {}, touched: {}, isSubmitting: false, }) const handleChange = (field: K, value: T[K]) => { setState((prev) => ({ ...prev, values: { ...prev.values, [field]: value }, errors: { ...prev.errors, [field]: undefined }, })) } const handleBlur = (field: K) => { setState((prev) => ({ ...prev, touched: { ...prev.touched, [field]: true }, })) } const handleSubmit = async (onSubmit: (values: T) => Promise) => { const errors = validate(state.values) if (Object.keys(errors).length > 0) { setState((prev) => ({ ...prev, errors, touched: Object.keys(state.values).reduce( (acc, key) => ({ ...acc, [key]: true }), {}, ), })) return } setState((prev) => ({ ...prev, isSubmitting: true })) try { await onSubmit(state.values) } finally { setState((prev) => ({ ...prev, isSubmitting: false })) } } return { values: state.values, errors: state.errors, touched: state.touched, isSubmitting: state.isSubmitting, handleChange, handleBlur, handleSubmit, } } // Usage interface LoginFormValues { email: string password: string } function LoginForm() { const form = useForm( { email: '', password: '' }, (values) => { const errors: Partial> = {} if (!values.email) { errors.email = 'Email is required' } if (!values.password) { errors.password = 'Password is required' } return errors }, ) return (
{ e.preventDefault() form.handleSubmit(async (values) => { await login(values.email, values.password) }) }} > form.handleChange('email', e.target.value)} onBlur={() => form.handleBlur('email')} /> {form.touched.email && form.errors.email && {form.errors.email}} form.handleChange('password', e.target.value)} onBlur={() => form.handleBlur('password')} /> {form.touched.password && form.errors.password && ( {form.errors.password} )}
) } ``` ## Validation Patterns ### Zod Integration ```typescript import { z } from 'zod' // Schema definition const userSchema = z.object({ id: z.string().uuid(), name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().min(0).max(120), role: z.enum(['admin', 'user', 'guest']), }) // Extract type from schema type User = z.infer // Validation function function validateUser(data: unknown): Result { const result = userSchema.safeParse(data) if (result.success) { return { success: true, data: result.data } } return { success: false, error: new Error(result.error.errors.map((e) => e.message).join(', ')), } } // API integration async function createUser(data: unknown): Promise> { const validation = validateUser(data) if (!validation.success) { return validation } try { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(validation.data), }) if (!response.ok) { return failure(new Error(`HTTP ${response.status}`)) } const user = await response.json() return success(user) } catch (error) { return failure(error instanceof Error ? error : new Error(String(error))) } } ``` ## Builder Pattern ```typescript // Fluent builder pattern class QueryBuilder { private filters: Array<(item: T) => boolean> = [] private sortFn?: (a: T, b: T) => number private limitValue?: number where(predicate: (item: T) => boolean): this { this.filters.push(predicate) return this } sortBy(compareFn: (a: T, b: T) => number): this { this.sortFn = compareFn return this } limit(count: number): this { this.limitValue = count return this } execute(data: T[]): T[] { let result = data // Apply filters this.filters.forEach((filter) => { result = result.filter(filter) }) // Apply sorting if (this.sortFn) { result = result.sort(this.sortFn) } // Apply limit if (this.limitValue !== undefined) { result = result.slice(0, this.limitValue) } return result } } // Usage interface Product { id: string name: string price: number category: string } const products: Product[] = [ /* ... */ ] const query = new QueryBuilder() .where((p) => p.category === 'electronics') .where((p) => p.price < 1000) .sortBy((a, b) => a.price - b.price) .limit(10) .execute(products) ``` ## Factory Pattern ```typescript // Abstract factory pattern with TypeScript interface Button { render: () => string onClick: () => void } interface ButtonFactory { createButton: (label: string, onClick: () => void) => Button } class PrimaryButton implements Button { constructor(private label: string, private clickHandler: () => void) {} render() { return `` } onClick() { this.clickHandler() } } class SecondaryButton implements Button { constructor(private label: string, private clickHandler: () => void) {} render() { return `` } onClick() { this.clickHandler() } } class PrimaryButtonFactory implements ButtonFactory { createButton(label: string, onClick: () => void): Button { return new PrimaryButton(label, onClick) } } class SecondaryButtonFactory implements ButtonFactory { createButton(label: string, onClick: () => void): Button { return new SecondaryButton(label, onClick) } } // Usage function createUI(factory: ButtonFactory) { const button = factory.createButton('Click me', () => console.log('Clicked!')) return button.render() } ``` ## Named Return Variables Pattern ```typescript // Following Go-style named returns function parseUser(data: unknown): { user: User | null; err: Error | null } { let user: User | null = null let err: Error | null = null try { user = userSchema.parse(data) } catch (error) { err = error instanceof Error ? error : new Error(String(error)) } return { user, err } } // With explicit naming function fetchData(url: string): { data: unknown | null status: number err: Error | null } { let data: unknown | null = null let status = 0 let err: Error | null = null try { const response = fetch(url) // Process response } catch (error) { err = error instanceof Error ? error : new Error(String(error)) } return { data, status, err } } ``` ## Best Practices 1. **Use discriminated unions** for type-safe state management 2. **Leverage generic types** for reusable components and hooks 3. **Extract types from Zod schemas** for runtime + compile-time safety 4. **Use Result/Option types** for explicit error handling 5. **Create builder patterns** for complex object construction 6. **Use factory patterns** for flexible object creation 7. **Type context properly** to catch usage errors at compile time 8. **Prefer const assertions** for immutable configurations 9. **Use branded types** for domain-specific primitives 10. **Document patterns** with JSDoc for team knowledge sharing