Best Practices
Best practices for using FoundryKit types effectively
Best Practices
Learn the best practices for using FoundryKit types to create maintainable, type-safe, and scalable applications.
Type Definition Best Practices
Use Descriptive Type Names
Create clear, descriptive names for your types that convey their purpose.
// ✅ Good: Clear and descriptive
interface UserProfileFormData {
personalInfo: {
firstName: string
lastName: string
email: string
}
preferences: {
theme: 'light' | 'dark'
notifications: boolean
}
}
interface CreateUserRequest {
name: string
email: string
age: number
}
interface UserApiResponse extends ApiResponse<User> {
user: User
}
// ❌ Avoid: Vague or unclear names
interface Data {
info: any
settings: any
}
interface Request {
stuff: any
}
interface Response {
result: any
}
Prefer Union Types over Enums
Use union types for better type inference and tree-shaking.
// ✅ Good: Union types with const assertions
const USER_ROLES = ['admin', 'user', 'guest'] as const
type UserRole = typeof USER_ROLES[number] // 'admin' | 'user' | 'guest'
const THEME_OPTIONS = ['light', 'dark', 'auto'] as const
type Theme = typeof THEME_OPTIONS[number] // 'light' | 'dark' | 'auto'
// ✅ Good: Direct union types
type Status = 'pending' | 'approved' | 'rejected'
type Size = 'sm' | 'md' | 'lg'
// ❌ Avoid: Enums (unless you need reverse mapping)
enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
Use Generic Constraints Effectively
Constrain generics to provide better type safety and IntelliSense.
// ✅ Good: Well-constrained generics
interface Repository<T extends { id: string }> {
findById(id: string): Promise<T | null>
create(data: Omit<T, 'id'>): Promise<T>
update(id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null>
delete(id: string): Promise<boolean>
}
interface EventHandler<T extends Record<string, any> = {}> {
type: string
payload: T
timestamp: Date
}
// Component props with constraints
interface ListProps<T extends { id: string | number }> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor?: (item: T) => string | number
}
// ❌ Avoid: Unconstrained generics
interface BadRepository<T> {
findById(id: any): Promise<T | null>
create(data: any): Promise<T>
}
interface BadEventHandler<T> {
type: any
payload: T
}
Component Type Patterns
Consistent Component Props Structure
Establish consistent patterns for component props.
import type { ComponentProps, WithChildren, WithClassName } from '@foundrykit/types'
// Base component props pattern
interface BaseComponentProps extends WithChildren, WithClassName {
id?: string
'data-testid'?: string
}
// Interactive component props pattern
interface InteractiveComponentProps extends BaseComponentProps {
disabled?: boolean
loading?: boolean
onClick?: EventHandler<React.MouseEvent>
onKeyDown?: EventHandler<React.KeyboardEvent>
}
// Form component props pattern
interface FormComponentProps extends InteractiveComponentProps {
name?: string
required?: boolean
error?: string
helperText?: string
}
// Specific component implementation
interface ButtonProps extends InteractiveComponentProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
type?: 'button' | 'submit' | 'reset'
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
}
// Usage with consistent patterns
function Button({
variant = 'primary',
size = 'md',
type = 'button',
disabled = false,
loading = false,
className,
children,
leftIcon,
rightIcon,
onClick,
...props
}: ButtonProps) {
return (
<button
type={type}
disabled={disabled || loading}
className={cn(
'btn',
`btn-${variant}`,
`btn-${size}`,
{ 'btn-disabled': disabled, 'btn-loading': loading },
className
)}
onClick={onClick}
{...props}
>
{leftIcon && <span className="btn-icon-left">{leftIcon}</span>}
{loading ? 'Loading...' : children}
{rightIcon && <span className="btn-icon-right">{rightIcon}</span>}
</button>
)
}
Polymorphic Component Patterns
Create flexible polymorphic components with proper type safety.
import type { ComponentProps, PolymorphicComponent } from '@foundrykit/types'
// Polymorphic component with constraints
type PolymorphicTextElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div'
interface TextProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
weight?: 'normal' | 'medium' | 'semibold' | 'bold'
color?: 'primary' | 'secondary' | 'muted' | 'error' | 'success'
align?: 'left' | 'center' | 'right'
truncate?: boolean
}
const Text: PolymorphicComponent<'p', TextProps> = ({
as: Component = 'p',
size = 'md',
weight = 'normal',
color = 'primary',
align = 'left',
truncate = false,
className,
children,
...props
}) => {
return (
<Component
className={cn(
'text',
`text-${size}`,
`text-weight-${weight}`,
`text-${color}`,
`text-${align}`,
{ 'text-truncate': truncate },
className
)}
{...props}
>
{children}
</Component>
)
}
// Usage with full type safety
function TextExamples() {
return (
<div>
{/* Default paragraph */}
<Text>Default paragraph text</Text>
{/* Heading with specific props */}
<Text as="h1" size="xl" weight="bold" color="primary">
Main heading
</Text>
{/* Span with truncation */}
<Text as="span" size="sm" color="muted" truncate>
This text will be truncated if too long
</Text>
{/* All HTML props are properly typed based on 'as' prop */}
<Text as="button" onClick={() => console.log('clicked')}>
Text as button (has onClick)
</Text>
</div>
)
}
API Type Patterns
Consistent API Response Structure
Establish consistent patterns for API responses.
import type { ApiResponse, ApiError, PaginatedResponse } from '@foundrykit/types'
// Standard API response patterns
interface StandardApiResponse<T> extends ApiResponse<T> {
data: T
success: true
message?: string
timestamp: string
requestId?: string
}
interface StandardApiError extends ApiError {
error: true
code: string
message: string
details?: Record<string, any>
timestamp: string
requestId?: string
path?: string
}
// Specific API response types
type GetUserResponse = StandardApiResponse<User>
type GetUsersResponse = PaginatedResponse<User>
type CreateUserResponse = StandardApiResponse<User>
type UpdateUserResponse = StandardApiResponse<User>
type DeleteUserResponse = StandardApiResponse<{ success: boolean }>
// API client with consistent error handling
class ApiClient {
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
try {
const response = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
})
const data = await response.json()
if (!response.ok) {
const error: StandardApiError = {
error: true,
code: data.code || 'HTTP_ERROR',
message: data.message || response.statusText,
details: data.details,
timestamp: data.timestamp || new Date().toISOString(),
requestId: data.requestId,
path: endpoint
}
throw error
}
return data
} catch (error) {
if (error && typeof error === 'object' && 'error' in error) {
throw error // Re-throw API errors
}
// Convert unknown errors to standard format
const standardError: StandardApiError = {
error: true,
code: 'NETWORK_ERROR',
message: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
path: endpoint
}
throw standardError
}
}
async getUser(id: string): Promise<GetUserResponse> {
return this.request<GetUserResponse>(`/api/users/${id}`)
}
async getUsers(params?: {
page?: number
limit?: number
search?: string
}): Promise<GetUsersResponse> {
const searchParams = new URLSearchParams()
if (params?.page) searchParams.set('page', String(params.page))
if (params?.limit) searchParams.set('limit', String(params.limit))
if (params?.search) searchParams.set('search', params.search)
const query = searchParams.toString()
return this.request<GetUsersResponse>(`/api/users${query ? `?${query}` : ''}`)
}
async createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
return this.request<CreateUserResponse>('/api/users', {
method: 'POST',
body: JSON.stringify(userData),
})
}
}
Type-safe API Error Handling
Create comprehensive error handling patterns.
// Error type hierarchy
interface BaseApiError extends ApiError {
error: true
code: string
message: string
timestamp: string
}
interface ValidationError extends BaseApiError {
code: 'VALIDATION_ERROR'
details: {
field: string
rule: string
value: any
message: string
}[]
}
interface NotFoundError extends BaseApiError {
code: 'NOT_FOUND'
details: {
resource: string
id: string
}
}
interface UnauthorizedError extends BaseApiError {
code: 'UNAUTHORIZED'
details?: {
reason: 'invalid_token' | 'expired_token' | 'missing_token'
}
}
interface RateLimitError extends BaseApiError {
code: 'RATE_LIMIT_EXCEEDED'
details: {
limit: number
remaining: number
resetTime: string
}
}
type ApiErrorType = ValidationError | NotFoundError | UnauthorizedError | RateLimitError
// Type guards for error handling
function isValidationError(error: ApiErrorType): error is ValidationError {
return error.code === 'VALIDATION_ERROR'
}
function isNotFoundError(error: ApiErrorType): error is NotFoundError {
return error.code === 'NOT_FOUND'
}
function isUnauthorizedError(error: ApiErrorType): error is UnauthorizedError {
return error.code === 'UNAUTHORIZED'
}
function isRateLimitError(error: ApiErrorType): error is RateLimitError {
return error.code === 'RATE_LIMIT_EXCEEDED'
}
// Error handling service
class ErrorHandlingService {
static handleApiError(error: ApiErrorType): string {
if (isValidationError(error)) {
const fieldErrors = error.details.map(detail =>
`${detail.field}: ${detail.message}`
).join(', ')
return `Validation failed: ${fieldErrors}`
}
if (isNotFoundError(error)) {
return `${error.details.resource} with ID ${error.details.id} not found`
}
if (isUnauthorizedError(error)) {
switch (error.details?.reason) {
case 'expired_token':
return 'Your session has expired. Please log in again.'
case 'invalid_token':
return 'Invalid authentication token. Please log in again.'
case 'missing_token':
return 'Authentication required. Please log in.'
default:
return 'You are not authorized to perform this action.'
}
}
if (isRateLimitError(error)) {
return `Rate limit exceeded. Try again after ${error.details.resetTime}`
}
return error.message || 'An unexpected error occurred'
}
static getErrorSeverity(error: ApiErrorType): 'low' | 'medium' | 'high' | 'critical' {
switch (error.code) {
case 'VALIDATION_ERROR':
return 'low'
case 'NOT_FOUND':
return 'medium'
case 'UNAUTHORIZED':
return 'high'
case 'RATE_LIMIT_EXCEEDED':
return 'medium'
default:
return 'critical'
}
}
}
// Usage in components
function UserForm() {
const [error, setError] = useState<ApiErrorType | null>(null)
const handleSubmit = async (userData: CreateUserRequest) => {
try {
await apiClient.createUser(userData)
// Handle success
} catch (err) {
const apiError = err as ApiErrorType
setError(apiError)
// Log error with severity
const severity = ErrorHandlingService.getErrorSeverity(apiError)
console.error(`API Error [${severity}]:`, apiError)
}
}
return (
<div>
{error && (
<div className={`alert alert-${ErrorHandlingService.getErrorSeverity(error)}`}>
{ErrorHandlingService.handleApiError(error)}
</div>
)}
{/* Form content */}
</div>
)
}
Form Type Patterns
Comprehensive Form Type System
Create a robust form type system with validation.
import type { FormField, ValidationResult, FieldValidator } from '@foundrykit/types'
// Form field configuration
interface FormFieldConfig<T = any> {
defaultValue: T
validation?: {
required?: boolean
minLength?: number
maxLength?: number
min?: number
max?: number
pattern?: RegExp
custom?: FieldValidator<T>
}
label?: string
placeholder?: string
helpText?: string
}
// Form schema definition
interface FormSchema<T extends Record<string, any>> {
[K in keyof T]: FormFieldConfig<T[K]>
}
// User form example
interface UserFormData {
name: string
email: string
age: number
bio: string
terms: boolean
}
const userFormSchema: FormSchema<UserFormData> = {
name: {
defaultValue: '',
validation: {
required: true,
minLength: 2,
maxLength: 50
},
label: 'Full Name',
placeholder: 'Enter your full name'
},
email: {
defaultValue: '',
validation: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
label: 'Email Address',
placeholder: 'Enter your email'
},
age: {
defaultValue: 0,
validation: {
required: true,
min: 18,
max: 120
},
label: 'Age',
placeholder: 'Enter your age'
},
bio: {
defaultValue: '',
validation: {
maxLength: 500
},
label: 'Bio',
placeholder: 'Tell us about yourself',
helpText: 'Maximum 500 characters'
},
terms: {
defaultValue: false,
validation: {
required: true,
custom: (value: boolean) => ({
isValid: value === true,
error: value ? undefined : 'You must accept the terms and conditions'
})
},
label: 'I accept the terms and conditions'
}
}
// Form validation service
class FormValidationService {
static validateField<T>(
value: T,
config: FormFieldConfig<T>
): { isValid: boolean; error?: string } {
const { validation } = config
if (!validation) return { isValid: true }
// Required validation
if (validation.required && (value === null || value === undefined || value === '')) {
return { isValid: false, error: `${config.label || 'Field'} is required` }
}
// String validations
if (typeof value === 'string') {
if (validation.minLength && value.length < validation.minLength) {
return {
isValid: false,
error: `${config.label || 'Field'} must be at least ${validation.minLength} characters`
}
}
if (validation.maxLength && value.length > validation.maxLength) {
return {
isValid: false,
error: `${config.label || 'Field'} must be no more than ${validation.maxLength} characters`
}
}
if (validation.pattern && !validation.pattern.test(value)) {
return {
isValid: false,
error: `${config.label || 'Field'} format is invalid`
}
}
}
// Number validations
if (typeof value === 'number') {
if (validation.min !== undefined && value < validation.min) {
return {
isValid: false,
error: `${config.label || 'Field'} must be at least ${validation.min}`
}
}
if (validation.max !== undefined && value > validation.max) {
return {
isValid: false,
error: `${config.label || 'Field'} must be no more than ${validation.max}`
}
}
}
// Custom validation
if (validation.custom) {
return validation.custom(value)
}
return { isValid: true }
}
static validateForm<T extends Record<string, any>>(
data: T,
schema: FormSchema<T>
): ValidationResult<T> {
const errors: Partial<Record<keyof T, string>> = {}
for (const [field, config] of Object.entries(schema) as Array<[keyof T, FormFieldConfig]>) {
const validation = this.validateField(data[field], config)
if (!validation.isValid && validation.error) {
errors[field] = validation.error
}
}
const isValid = Object.keys(errors).length === 0
return {
isValid,
errors: errors as Record<keyof T, string>,
data: isValid ? data : null
}
}
}
// Type-safe form hook
function useTypedForm<T extends Record<string, any>>(
schema: FormSchema<T>
) {
const [data, setData] = useState<T>(() => {
const initialData = {} as T
for (const [field, config] of Object.entries(schema) as Array<[keyof T, FormFieldConfig]>) {
initialData[field] = config.defaultValue
}
return initialData
})
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const updateField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
setData(prev => ({ ...prev, [field]: value }))
// Validate field on change if it's been touched
if (touched[field]) {
const validation = FormValidationService.validateField(value, schema[field])
setErrors(prev => ({
...prev,
[field]: validation.error
}))
}
}, [schema, touched])
const touchField = useCallback(<K extends keyof T>(field: K) => {
setTouched(prev => ({ ...prev, [field]: true }))
// Validate field when touched
const validation = FormValidationService.validateField(data[field], schema[field])
if (!validation.isValid) {
setErrors(prev => ({ ...prev, [field]: validation.error }))
}
}, [data, schema])
const validateForm = useCallback(() => {
const validation = FormValidationService.validateForm(data, schema)
setErrors(validation.errors)
return validation
}, [data, schema])
const reset = useCallback(() => {
const initialData = {} as T
for (const [field, config] of Object.entries(schema) as Array<[keyof T, FormFieldConfig]>) {
initialData[field] = config.defaultValue
}
setData(initialData)
setErrors({})
setTouched({})
}, [schema])
return {
data,
errors,
touched,
updateField,
touchField,
validateForm,
reset,
isValid: Object.keys(errors).length === 0
}
}
// Usage
function UserForm() {
const {
data,
errors,
touched,
updateField,
touchField,
validateForm,
reset,
isValid
} = useTypedForm(userFormSchema)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const validation = validateForm()
if (validation.isValid && validation.data) {
try {
await apiClient.createUser(validation.data)
reset()
} catch (error) {
console.error('Failed to create user:', error)
}
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>{userFormSchema.name.label}</label>
<input
value={data.name}
onChange={(e) => updateField('name', e.target.value)}
onBlur={() => touchField('name')}
placeholder={userFormSchema.name.placeholder}
/>
{touched.name && errors.name && (
<span className="error">{errors.name}</span>
)}
</div>
<div>
<label>{userFormSchema.email.label}</label>
<input
type="email"
value={data.email}
onChange={(e) => updateField('email', e.target.value)}
onBlur={() => touchField('email')}
placeholder={userFormSchema.email.placeholder}
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<label>{userFormSchema.age.label}</label>
<input
type="number"
value={data.age}
onChange={(e) => updateField('age', parseInt(e.target.value) || 0)}
onBlur={() => touchField('age')}
placeholder={userFormSchema.age.placeholder}
/>
{touched.age && errors.age && (
<span className="error">{errors.age}</span>
)}
</div>
<div>
<label>{userFormSchema.bio.label}</label>
<textarea
value={data.bio}
onChange={(e) => updateField('bio', e.target.value)}
onBlur={() => touchField('bio')}
placeholder={userFormSchema.bio.placeholder}
/>
{userFormSchema.bio.helpText && (
<small>{userFormSchema.bio.helpText}</small>
)}
{touched.bio && errors.bio && (
<span className="error">{errors.bio}</span>
)}
</div>
<div>
<label>
<input
type="checkbox"
checked={data.terms}
onChange={(e) => updateField('terms', e.target.checked)}
onBlur={() => touchField('terms')}
/>
{userFormSchema.terms.label}
</label>
{touched.terms && errors.terms && (
<span className="error">{errors.terms}</span>
)}
</div>
<button type="submit" disabled={!isValid}>
Submit
</button>
<button type="button" onClick={reset}>
Reset
</button>
</form>
)
}
Testing Best Practices
Type-safe Test Utilities
Create comprehensive testing utilities with full type safety.
import type { ApiResponse, ApiError, ValidationResult } from '@foundrykit/types'
// Test data factories
interface TestDataFactory<T> {
create(overrides?: Partial<T>): T
createMany(count: number, overrides?: Partial<T>): T[]
}
class UserTestFactory implements TestDataFactory<User> {
private static defaultUser: User = {
id: '1',
name: 'John Doe',
email: 'john.doe@example.com',
age: 30,
createdAt: new Date(),
updatedAt: new Date()
}
create(overrides: Partial<User> = {}): User {
return {
...UserTestFactory.defaultUser,
...overrides,
id: overrides.id || `user-${Math.random().toString(36).substr(2, 9)}`
}
}
createMany(count: number, overrides: Partial<User> = {}): User[] {
return Array.from({ length: count }, (_, index) =>
this.create({ ...overrides, name: `${overrides.name || 'User'} ${index + 1}` })
)
}
}
// Mock API response factories
class ApiResponseFactory {
static success<T>(data: T, message?: string): ApiResponse<T> {
return {
data,
success: true,
message,
timestamp: new Date().toISOString()
}
}
static error(code: string, message: string, details?: any): ApiError {
return {
error: true,
code,
message,
details,
timestamp: new Date().toISOString()
}
}
static paginated<T>(
data: T[],
page: number = 1,
limit: number = 10,
total?: number
): PaginatedResponse<T> {
const actualTotal = total ?? data.length
return {
data,
success: true,
timestamp: new Date().toISOString(),
pagination: {
page,
limit,
total: actualTotal,
totalPages: Math.ceil(actualTotal / limit),
hasNextPage: page * limit < actualTotal,
hasPrevPage: page > 1
}
}
}
}
// Type-safe test assertions
class TypedAssertions {
static isApiResponse<T>(response: any): asserts response is ApiResponse<T> {
expect(response).toHaveProperty('data')
expect(response).toHaveProperty('success', true)
expect(response).toHaveProperty('timestamp')
expect(typeof response.timestamp).toBe('string')
}
static isApiError(error: any): asserts error is ApiError {
expect(error).toHaveProperty('error', true)
expect(error).toHaveProperty('code')
expect(error).toHaveProperty('message')
expect(error).toHaveProperty('timestamp')
expect(typeof error.code).toBe('string')
expect(typeof error.message).toBe('string')
}
static isPaginatedResponse<T>(response: any): asserts response is PaginatedResponse<T> {
this.isApiResponse(response)
expect(response).toHaveProperty('pagination')
expect(response.pagination).toHaveProperty('page')
expect(response.pagination).toHaveProperty('limit')
expect(response.pagination).toHaveProperty('total')
expect(response.pagination).toHaveProperty('totalPages')
expect(response.pagination).toHaveProperty('hasNextPage')
expect(response.pagination).toHaveProperty('hasPrevPage')
}
static isValidationResult<T>(result: any): asserts result is ValidationResult<T> {
expect(result).toHaveProperty('isValid')
expect(result).toHaveProperty('errors')
expect(typeof result.isValid).toBe('boolean')
expect(typeof result.errors).toBe('object')
}
}
// Usage in tests
describe('User API', () => {
const userFactory = new UserTestFactory()
it('should return paginated users', async () => {
const mockUsers = userFactory.createMany(5)
const mockResponse = ApiResponseFactory.paginated(mockUsers, 1, 10, 25)
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse)
})
const apiClient = new UserApiClient()
const response = await apiClient.getUsers({ page: 1, limit: 10 })
// Type-safe assertions
TypedAssertions.isPaginatedResponse(response)
expect(response.data).toHaveLength(5)
expect(response.pagination.page).toBe(1)
expect(response.pagination.total).toBe(25)
})
it('should handle API errors correctly', async () => {
const mockError = ApiResponseFactory.error('NOT_FOUND', 'User not found')
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
json: () => Promise.resolve(mockError)
})
const apiClient = new UserApiClient()
await expect(apiClient.getUser('invalid-id')).rejects.toThrow()
try {
await apiClient.getUser('invalid-id')
} catch (error) {
TypedAssertions.isApiError(error)
expect(error.code).toBe('NOT_FOUND')
expect(error.message).toBe('User not found')
}
})
})
describe('Form Validation', () => {
const userFactory = new UserTestFactory()
it('should validate user data correctly', () => {
const validUser = userFactory.create()
const result = FormValidationService.validateForm(validUser, userFormSchema)
TypedAssertions.isValidationResult(result)
expect(result.isValid).toBe(true)
expect(Object.keys(result.errors)).toHaveLength(0)
expect(result.data).toEqual(validUser)
})
it('should return validation errors for invalid data', () => {
const invalidUser = userFactory.create({
name: '', // Invalid: required field
email: 'invalid-email', // Invalid: wrong format
age: 15 // Invalid: below minimum
})
const result = FormValidationService.validateForm(invalidUser, userFormSchema)
TypedAssertions.isValidationResult(result)
expect(result.isValid).toBe(false)
expect(result.errors.name).toBeTruthy()
expect(result.errors.email).toBeTruthy()
expect(result.errors.age).toBeTruthy()
expect(result.data).toBeNull()
})
})
Common Anti-Patterns
What to Avoid
// ❌ Don't use 'any' type
interface BadApiResponse {
data: any
status: any
}
// ❌ Don't create overly complex generic constraints
interface BadGeneric<T extends Record<string, Record<string, Record<string, any>>>> {
data: T
}
// ❌ Don't ignore null/undefined possibilities
interface BadUser {
id: string
name: string
email: string
avatar: string // Should be optional or nullable
}
// ❌ Don't create inconsistent naming patterns
interface user_data {
UserID: string
user_name: string
Email: string
}
// ❌ Don't use enums when union types are better
enum BadStatus {
LOADING = 'loading',
SUCCESS = 'success',
ERROR = 'error'
}
// ❌ Don't create types that are too specific
interface VerySpecificButtonProps {
isBlueRoundedButtonWithShadowAndHover: boolean
}
// ✅ Better alternatives
interface ApiResponse<T> {
data: T
success: boolean
message?: string
}
interface SimpleGeneric<T extends Record<string, unknown>> {
data: T
}
interface User {
id: string
name: string
email: string
avatar?: string | null
}
interface UserData {
id: string
name: string
email: string
}
type Status = 'loading' | 'success' | 'error'
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger'
size: 'sm' | 'md' | 'lg'
rounded?: boolean
shadow?: boolean
}
Best Practices Checklist
Type Definition Checklist
- Use descriptive, clear type names
- Prefer union types over enums
- Use generic constraints appropriately
- Make optional properties explicit
- Avoid using
any
type - Use consistent naming conventions
- Document complex types with comments
Component Type Checklist
- Extend base component prop types
- Use polymorphic patterns when appropriate
- Implement proper ref forwarding
- Handle event types correctly
- Use consistent prop patterns
- Provide default values for optional props
API Type Checklist
- Use consistent response structures
- Implement proper error typing
- Use type guards for error handling
- Handle pagination consistently
- Type request/response pairs together
- Document API contracts with types
Form Type Checklist
- Use comprehensive validation types
- Implement field-level validation
- Handle form state properly
- Use type-safe form hooks
- Provide clear error messages
- Handle async validation
Testing Type Checklist
- Create type-safe test utilities
- Use proper assertion types
- Mock API responses with correct types
- Test type guards and predicates
- Use factory patterns for test data
Next Steps
- Explore the type definitions reference for detailed type documentation
- Learn about custom type patterns for advanced usage
- Review integration guide for framework-specific usage