Custom Type Patterns
Advanced patterns and techniques for creating custom types
Custom Type Patterns
Learn advanced patterns and techniques for creating custom types that enhance type safety and developer experience in your applications.
Generic Type Patterns
Generic Component Props
Create flexible, reusable component types with generics.
import type { ComponentProps, WithChildren } from '@foundrykit/types'
// Generic list component
interface ListProps<T> extends WithChildren {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
keyExtractor: (item: T) => string | number
className?: string
emptyMessage?: string
}
function List<T>({
items,
renderItem,
keyExtractor,
className,
emptyMessage = 'No items found',
children
}: ListProps<T>) {
if (items.length === 0) {
return <div className="empty-state">{emptyMessage}</div>
}
return (
<div className={className}>
{children}
{items.map((item, index) => (
<div key={keyExtractor(item)}>
{renderItem(item, index)}
</div>
))}
</div>
)
}
// Usage with type inference
interface User {
id: string
name: string
email: string
}
function UserList({ users }: { users: User[] }) {
return (
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user, index) => (
<div className="user-item">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
emptyMessage="No users found"
/>
)
}
Generic Data Fetching
Create type-safe data fetching patterns.
import type { ApiResponse, ApiError } from '@foundrykit/types'
// Generic data fetching hook
interface UseDataOptions<T> {
enabled?: boolean
refetchOnWindowFocus?: boolean
staleTime?: number
cacheTime?: number
onSuccess?: (data: T) => void
onError?: (error: ApiError) => void
}
interface UseDataResult<T> {
data: T | null
loading: boolean
error: ApiError | null
refetch: () => Promise<void>
mutate: (data: T) => void
}
function useData<T>(
fetcher: () => Promise<ApiResponse<T>>,
options: UseDataOptions<T> = {}
): UseDataResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const fetchData = useCallback(async () => {
if (!options.enabled && options.enabled !== undefined) return
setLoading(true)
setError(null)
try {
const response = await fetcher()
setData(response.data)
options.onSuccess?.(response.data)
} catch (err) {
const apiError = err as ApiError
setError(apiError)
options.onError?.(apiError)
} finally {
setLoading(false)
}
}, [fetcher, options])
const mutate = useCallback((newData: T) => {
setData(newData)
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
return {
data,
loading,
error,
refetch: fetchData,
mutate
}
}
// Usage with type inference
interface User {
id: string
name: string
email: string
}
function UserProfile({ userId }: { userId: string }) {
const {
data: user,
loading,
error,
refetch
} = useData<User>(
() => fetchUser(userId),
{
enabled: !!userId,
onSuccess: (user) => {
console.log(`Loaded user: ${user.name}`)
},
onError: (error) => {
console.error('Failed to load user:', error.message)
}
}
)
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (!user) return <div>User not found</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={refetch}>Refresh</button>
</div>
)
}
Builder Pattern Types
Fluent API Builder
Create type-safe builder patterns for complex configurations.
// Query builder pattern
interface QueryBuilder<T> {
where<K extends keyof T>(field: K, operator: 'eq' | 'ne' | 'gt' | 'lt' | 'in', value: T[K] | T[K][]): QueryBuilder<T>
orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>
limit(count: number): QueryBuilder<T>
offset(count: number): QueryBuilder<T>
build(): QueryConfig<T>
}
interface QueryConfig<T> {
filters: Array<{
field: keyof T
operator: string
value: any
}>
sorting: Array<{
field: keyof T
direction: 'asc' | 'desc'
}>
pagination: {
limit?: number
offset?: number
}
}
class TypedQueryBuilder<T> implements QueryBuilder<T> {
private config: QueryConfig<T> = {
filters: [],
sorting: [],
pagination: {}
}
where<K extends keyof T>(
field: K,
operator: 'eq' | 'ne' | 'gt' | 'lt' | 'in',
value: T[K] | T[K][]
): QueryBuilder<T> {
this.config.filters.push({ field, operator, value })
return this
}
orderBy<K extends keyof T>(
field: K,
direction: 'asc' | 'desc' = 'asc'
): QueryBuilder<T> {
this.config.sorting.push({ field, direction })
return this
}
limit(count: number): QueryBuilder<T> {
this.config.pagination.limit = count
return this
}
offset(count: number): QueryBuilder<T> {
this.config.pagination.offset = count
return this
}
build(): QueryConfig<T> {
return { ...this.config }
}
}
// Factory function
function createQuery<T>(): QueryBuilder<T> {
return new TypedQueryBuilder<T>()
}
// Usage with full type safety
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
const query = createQuery<Product>()
.where('category', 'eq', 'electronics')
.where('price', 'gt', 100)
.where('inStock', 'eq', true)
.orderBy('price', 'desc')
.orderBy('name', 'asc')
.limit(20)
.offset(0)
.build()
// Type-safe query execution
async function executeQuery<T>(config: QueryConfig<T>): Promise<T[]> {
// Implementation would convert config to actual query
console.log('Executing query:', config)
return []
}
const products = await executeQuery<Product>(query)
Configuration Builder
Create type-safe configuration builders.
// Component configuration builder
interface ComponentConfig {
theme: 'light' | 'dark'
size: 'sm' | 'md' | 'lg'
variant: 'primary' | 'secondary' | 'danger'
disabled: boolean
loading: boolean
className?: string
}
interface ConfigBuilder<T> {
set<K extends keyof T>(key: K, value: T[K]): ConfigBuilder<T>
merge(partial: Partial<T>): ConfigBuilder<T>
build(): T
}
class TypedConfigBuilder<T> implements ConfigBuilder<T> {
constructor(private config: T) {}
set<K extends keyof T>(key: K, value: T[K]): ConfigBuilder<T> {
return new TypedConfigBuilder({ ...this.config, [key]: value })
}
merge(partial: Partial<T>): ConfigBuilder<T> {
return new TypedConfigBuilder({ ...this.config, ...partial })
}
build(): T {
return this.config
}
}
// Factory with defaults
function createComponentConfig(): ConfigBuilder<ComponentConfig> {
const defaults: ComponentConfig = {
theme: 'light',
size: 'md',
variant: 'primary',
disabled: false,
loading: false
}
return new TypedConfigBuilder(defaults)
}
// Usage
const buttonConfig = createComponentConfig()
.set('theme', 'dark')
.set('size', 'lg')
.set('variant', 'danger')
.merge({ disabled: false, loading: true })
.build()
// Use config in component
function ConfigurableButton({ config, children }: {
config: ComponentConfig
children: React.ReactNode
}) {
return (
<button
className={cn(
'btn',
`btn-${config.theme}`,
`btn-${config.size}`,
`btn-${config.variant}`,
{
'btn-disabled': config.disabled,
'btn-loading': config.loading
},
config.className
)}
disabled={config.disabled || config.loading}
>
{config.loading ? 'Loading...' : children}
</button>
)
}
State Machine Types
Finite State Machine
Create type-safe state machines.
// State machine definition
interface StateMachine<TState extends string, TEvent extends string> {
initial: TState
states: Record<TState, StateDefinition<TState, TEvent>>
}
interface StateDefinition<TState extends string, TEvent extends string> {
on?: Partial<Record<TEvent, TState>>
entry?: () => void
exit?: () => void
}
interface StateContext<TState extends string, TEvent extends string> {
current: TState
send: (event: TEvent) => void
can: (event: TEvent) => boolean
}
// Form state machine example
type FormState = 'idle' | 'validating' | 'submitting' | 'success' | 'error'
type FormEvent = 'VALIDATE' | 'SUBMIT' | 'SUCCESS' | 'ERROR' | 'RESET'
const formStateMachine: StateMachine<FormState, FormEvent> = {
initial: 'idle',
states: {
idle: {
on: {
VALIDATE: 'validating',
SUBMIT: 'submitting'
}
},
validating: {
on: {
SUBMIT: 'submitting',
ERROR: 'error',
RESET: 'idle'
}
},
submitting: {
on: {
SUCCESS: 'success',
ERROR: 'error'
},
entry: () => console.log('Starting submission...'),
exit: () => console.log('Submission completed')
},
success: {
on: {
RESET: 'idle'
}
},
error: {
on: {
VALIDATE: 'validating',
RESET: 'idle'
}
}
}
}
// State machine hook
function useStateMachine<TState extends string, TEvent extends string>(
machine: StateMachine<TState, TEvent>
): StateContext<TState, TEvent> {
const [current, setCurrent] = useState<TState>(machine.initial)
const send = useCallback((event: TEvent) => {
const currentState = machine.states[current]
const nextState = currentState.on?.[event]
if (nextState) {
// Call exit handler
currentState.exit?.()
// Transition to next state
setCurrent(nextState)
// Call entry handler
machine.states[nextState].entry?.()
}
}, [current, machine])
const can = useCallback((event: TEvent) => {
const currentState = machine.states[current]
return !!currentState.on?.[event]
}, [current, machine])
return { current, send, can }
}
// Usage in component
function FormWithStateMachine() {
const { current, send, can } = useStateMachine(formStateMachine)
const [formData, setFormData] = useState({ name: '', email: '' })
const handleValidate = () => {
if (can('VALIDATE')) {
send('VALIDATE')
// Perform validation logic
setTimeout(() => {
const isValid = formData.name && formData.email
send(isValid ? 'RESET' : 'ERROR')
}, 1000)
}
}
const handleSubmit = () => {
if (can('SUBMIT')) {
send('SUBMIT')
// Perform submission logic
setTimeout(() => {
const success = Math.random() > 0.5
send(success ? 'SUCCESS' : 'ERROR')
}, 2000)
}
}
return (
<div>
<div>Current state: {current}</div>
<input
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Name"
disabled={current === 'submitting'}
/>
<input
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="Email"
disabled={current === 'submitting'}
/>
<button
onClick={handleValidate}
disabled={!can('VALIDATE')}
>
Validate
</button>
<button
onClick={handleSubmit}
disabled={!can('SUBMIT')}
>
{current === 'submitting' ? 'Submitting...' : 'Submit'}
</button>
<button
onClick={() => send('RESET')}
disabled={!can('RESET')}
>
Reset
</button>
{current === 'success' && (
<div className="success">Form submitted successfully!</div>
)}
{current === 'error' && (
<div className="error">Please check your inputs and try again.</div>
)}
</div>
)
}
Plugin System Types
Extensible Plugin Architecture
Create type-safe plugin systems.
// Plugin system types
interface Plugin<TConfig = unknown, TApi = unknown> {
name: string
version: string
config?: TConfig
install: (api: TApi) => void | Promise<void>
uninstall?: (api: TApi) => void | Promise<void>
}
interface PluginManager<TApi> {
register<TConfig>(plugin: Plugin<TConfig, TApi>): Promise<void>
unregister(name: string): Promise<void>
getPlugin(name: string): Plugin<unknown, TApi> | undefined
getPlugins(): Plugin<unknown, TApi>[]
}
// Example: Form validation plugin system
interface FormValidationApi {
addValidator: (name: string, validator: FieldValidator) => void
removeValidator: (name: string) => void
validate: (value: any, rules: string[]) => ValidationResult
}
interface FieldValidator {
validate: (value: any, options?: any) => { isValid: boolean; error?: string }
}
interface ValidationResult {
isValid: boolean
errors: string[]
}
// Email validation plugin
interface EmailValidatorConfig {
allowInternational?: boolean
requireTLD?: boolean
}
const emailValidatorPlugin: Plugin<EmailValidatorConfig, FormValidationApi> = {
name: 'email-validator',
version: '1.0.0',
config: {
allowInternational: true,
requireTLD: true
},
install: async (api) => {
api.addValidator('email', {
validate: (value: string, options: EmailValidatorConfig = {}) => {
const { allowInternational = true, requireTLD = true } = options
let regex = /^[^\s@]+@[^\s@]+/
if (requireTLD) {
regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
}
if (!allowInternational) {
regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
}
const isValid = regex.test(value)
return {
isValid,
error: isValid ? undefined : 'Please enter a valid email address'
}
}
})
},
uninstall: async (api) => {
api.removeValidator('email')
}
}
// Plugin manager implementation
class FormValidationManager implements PluginManager<FormValidationApi> {
private plugins = new Map<string, Plugin<unknown, FormValidationApi>>()
private validators = new Map<string, FieldValidator>()
private api: FormValidationApi = {
addValidator: (name, validator) => {
this.validators.set(name, validator)
},
removeValidator: (name) => {
this.validators.delete(name)
},
validate: (value, rules) => {
const errors: string[] = []
for (const rule of rules) {
const validator = this.validators.get(rule)
if (validator) {
const result = validator.validate(value)
if (!result.isValid && result.error) {
errors.push(result.error)
}
}
}
return {
isValid: errors.length === 0,
errors
}
}
}
async register<TConfig>(plugin: Plugin<TConfig, FormValidationApi>): Promise<void> {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin ${plugin.name} is already registered`)
}
await plugin.install(this.api)
this.plugins.set(plugin.name, plugin as Plugin<unknown, FormValidationApi>)
}
async unregister(name: string): Promise<void> {
const plugin = this.plugins.get(name)
if (plugin && plugin.uninstall) {
await plugin.uninstall(this.api)
}
this.plugins.delete(name)
}
getPlugin(name: string): Plugin<unknown, FormValidationApi> | undefined {
return this.plugins.get(name)
}
getPlugins(): Plugin<unknown, FormValidationApi>[] {
return Array.from(this.plugins.values())
}
// Expose validation API
validate(value: any, rules: string[]): ValidationResult {
return this.api.validate(value, rules)
}
}
// Usage
const validationManager = new FormValidationManager()
// Register plugins
await validationManager.register(emailValidatorPlugin)
// Use validation
const emailResult = validationManager.validate('user@example.com', ['email'])
console.log(emailResult) // { isValid: true, errors: [] }
const invalidEmailResult = validationManager.validate('invalid-email', ['email'])
console.log(invalidEmailResult) // { isValid: false, errors: ['Please enter a valid email address'] }
Advanced Generic Patterns
Conditional Type Chains
Create complex conditional type logic.
// Conditional type for API responses
type ApiResponseType<T> =
T extends 'user' ? User :
T extends 'users' ? User[] :
T extends 'product' ? Product :
T extends 'products' ? Product[] :
T extends 'order' ? Order :
T extends 'orders' ? Order[] :
never
// Generic API client
interface ApiClient {
get<T extends string>(endpoint: T): Promise<ApiResponseType<T>>
}
class TypedApiClient implements ApiClient {
async get<T extends string>(endpoint: T): Promise<ApiResponseType<T>> {
const response = await fetch(`/api/${endpoint}`)
return response.json() as Promise<ApiResponseType<T>>
}
}
// Usage with full type inference
const client = new TypedApiClient()
// TypeScript infers return type as User
const user = await client.get('user')
// TypeScript infers return type as User[]
const users = await client.get('users')
// TypeScript infers return type as Product
const product = await client.get('product')
Recursive Type Patterns
Create types that work with nested structures.
// Recursive menu structure
interface MenuItem {
id: string
label: string
icon?: string
href?: string
children?: MenuItem[]
}
// Recursive type to flatten menu structure
type FlatMenuItem<T extends MenuItem> = {
id: T['id']
label: T['label']
icon: T['icon']
href: T['href']
level: number
path: string[]
} & (T['children'] extends MenuItem[]
? { children: FlatMenuItem<T['children'][number]>[] }
: { children?: never }
)
// Recursive function to flatten menu
function flattenMenu(items: MenuItem[], level = 0, path: string[] = []): FlatMenuItem<MenuItem>[] {
return items.map(item => ({
id: item.id,
label: item.label,
icon: item.icon,
href: item.href,
level,
path: [...path, item.id],
children: item.children ? flattenMenu(item.children, level + 1, [...path, item.id]) : undefined
})) as FlatMenuItem<MenuItem>[]
}
// Tree traversal types
type TreeNode<T> = T & {
children?: TreeNode<T>[]
}
type TreeTraversal<T> = {
preOrder: (node: TreeNode<T>) => void
postOrder: (node: TreeNode<T>) => void
inOrder: (node: TreeNode<T>) => void
breadthFirst: (node: TreeNode<T>) => void
}
function createTreeTraversal<T>(): TreeTraversal<T> {
return {
preOrder: (root) => {
const visit = (node: TreeNode<T>) => {
console.log(node)
node.children?.forEach(visit)
}
visit(root)
},
postOrder: (root) => {
const visit = (node: TreeNode<T>) => {
node.children?.forEach(visit)
console.log(node)
}
visit(root)
},
inOrder: (root) => {
const visit = (node: TreeNode<T>) => {
const children = node.children || []
const mid = Math.floor(children.length / 2)
children.slice(0, mid).forEach(visit)
console.log(node)
children.slice(mid).forEach(visit)
}
visit(root)
},
breadthFirst: (root) => {
const queue: TreeNode<T>[] = [root]
while (queue.length > 0) {
const node = queue.shift()!
console.log(node)
if (node.children) {
queue.push(...node.children)
}
}
}
}
}
Next Steps
- Review integration guide for framework-specific usage
- Check out best practices for optimal type usage
- Explore the type definitions reference for detailed type documentation