diff --git a/docs/TYPE_SAFETY_MIGRATION.md b/docs/TYPE_SAFETY_MIGRATION.md new file mode 100644 index 00000000..b21f5bdf --- /dev/null +++ b/docs/TYPE_SAFETY_MIGRATION.md @@ -0,0 +1,186 @@ +# Type Safety Migration Guide + +## ✅ Phase 1: Core Infrastructure (COMPLETE) + +### Error Handling Utility +- ✅ Added `getErrorMessage(error: unknown)` to `src/lib/errorHandler.ts` +- ✅ Use this instead of `catch (error: any)` throughout codebase + +### Form Image Upload Types +- ✅ Created `UploadedImage` interface in `src/types/company.ts` +- ✅ Updated all 4 company forms (Manufacturer, Designer, Operator, PropertyOwner) + +### Temporary Entity States +- ✅ Created `TempCompanyData` interface in `src/types/company.ts` +- ✅ Created `TempRideModelData` interface in `src/types/company.ts` +- ✅ Updated `RideForm.tsx` to use proper types + +### Submission Item Types +- ✅ Updated `SubmissionItemData` in `src/types/submissions.ts` to use `Record` instead of `any` + +### ESLint Strict Rules +- ✅ Enabled strict TypeScript rules in `eslint.config.js`: + - `@typescript-eslint/no-explicit-any`: error + - `@typescript-eslint/no-unsafe-assignment`: error + - `@typescript-eslint/no-unsafe-member-access`: error + - `@typescript-eslint/no-unsafe-call`: error + - `@typescript-eslint/no-unsafe-return`: error + +--- + +## 🚧 Phase 2: Replace Catch Blocks (86 occurrences) + +### Migration Pattern + +**Before:** +```typescript +} catch (error: any) { + toast({ description: error.message, variant: 'destructive' }); +} +``` + +**After:** +```typescript +import { getErrorMessage } from '@/lib/errorHandler'; + +} catch (error) { + toast({ description: getErrorMessage(error), variant: 'destructive' }); +} +``` + +### Files with catch blocks (46 files): + +#### Auth Components (7 files) +- [ ] `src/components/auth/AuthButtons.tsx` (1) +- [ ] `src/components/auth/AuthModal.tsx` (4) +- [ ] `src/components/auth/MFAChallenge.tsx` (2) +- [ ] `src/components/auth/MFARemovalDialog.tsx` (3) +- [ ] `src/components/auth/TOTPSetup.tsx` (3) + +#### Admin Components (3 files) +- [ ] `src/components/admin/NovuMigrationUtility.tsx` (1) +- [ ] `src/components/admin/ParkForm.tsx` (1) +- [ ] `src/components/admin/RideForm.tsx` (1) + +#### Moderation Components (6 files) +- [ ] `src/components/moderation/ConflictResolutionDialog.tsx` (1) +- [ ] `src/components/moderation/ItemEditDialog.tsx` (1) +- [ ] `src/components/moderation/PhotoSubmissionDisplay.tsx` (1) +- [ ] `src/components/moderation/ReassignDialog.tsx` (1) +- [ ] `src/components/moderation/RecentActivity.tsx` (1) +- [ ] `src/components/moderation/SubmissionReviewManager.tsx` (6) + +#### Settings Components (4 files) +- [ ] `src/components/settings/DeletionStatusBanner.tsx` (1) +- [ ] `src/components/settings/EmailChangeDialog.tsx` (1) +- [ ] `src/components/settings/PasswordUpdateDialog.tsx` (3) +- [ ] `src/components/settings/SimplePhotoUpload.tsx` (1) + +#### Other Components (5 files) +- [ ] `src/components/profile/UserBlockButton.tsx` (1) +- [ ] `src/components/timeline/EntityTimelineManager.tsx` (1) +- [ ] `src/components/timeline/TimelineEventEditorDialog.tsx` (1) +- [ ] `src/components/upload/PhotoUpload.tsx` (1) + +#### Hooks (4 files) +- [ ] `src/hooks/moderation/useModerationActions.ts` (4) +- [ ] `src/hooks/moderation/useModerationQueueManager.ts` (4) +- [ ] `src/hooks/useEntityVersions.ts` (3) +- [ ] `src/hooks/useModerationQueue.ts` (5) + +#### Services (3 files) +- [ ] `src/lib/conflictResolutionService.ts` (1) +- [ ] `src/lib/identityService.ts` (4) +- [ ] `src/lib/submissionItemsService.ts` (1) + +#### Pages (14 files) +- [ ] `src/pages/Auth.tsx` (4) +- [ ] `src/pages/AuthCallback.tsx` (2) +- [ ] `src/pages/DesignerDetail.tsx` (1) +- [ ] `src/pages/Designers.tsx` (1) +- [ ] `src/pages/ManufacturerDetail.tsx` (1) +- [ ] `src/pages/Manufacturers.tsx` (1) +- [ ] `src/pages/OperatorDetail.tsx` (1) +- [ ] `src/pages/Operators.tsx` (1) +- [ ] `src/pages/ParkDetail.tsx` (2) +- [ ] `src/pages/ParkOwners.tsx` (1) +- [ ] `src/pages/ParkRides.tsx` (1) +- [ ] `src/pages/Profile.tsx` (6) +- [ ] `src/pages/PropertyOwnerDetail.tsx` (1) +- [ ] `src/pages/RideDetail.tsx` (1) +- [ ] (+ more pages...) + +--- + +## 🚧 Phase 3: Fix `as any` Type Assertions (76 occurrences) + +### Categories to Address: + +#### 1. Dynamic Table Queries (6 files) +Pattern: `.from(tableName as any)` +- [ ] `src/hooks/moderation/useEntityCache.ts` +- [ ] `src/hooks/useEntityVersions.ts` +- [ ] `src/lib/entityValidationSchemas.ts` +- [ ] `src/lib/moderation/actions.ts` +- [ ] `src/lib/versioningUtils.ts` + +**Solution:** Create type-safe table name union and query builder + +#### 2. Submission Content Access (5 files) +Pattern: `submission.content as any` +- [ ] `src/hooks/moderation/useEntityCache.ts` +- [ ] `src/hooks/moderation/useRealtimeSubscriptions.ts` +- [ ] `src/lib/moderation/entities.ts` +- [ ] `src/lib/moderation/realtime.ts` +- [ ] `src/lib/systemActivityService.ts` + +**Solution:** Use `ContentSubmissionContent` type with proper type guards + +#### 3. Entity Submission Helpers (1 file) +Pattern: `images: processedImages as any` +- [ ] `src/lib/entitySubmissionHelpers.ts` (6 occurrences) + +**Solution:** Define proper `ProcessedImages` interface + +#### 4. Component-Specific (13 files) +Various patterns requiring individual solutions: +- [ ] `src/components/reviews/ReviewsList.tsx` +- [ ] `src/components/rides/SimilarRides.tsx` +- [ ] `src/components/moderation/SubmissionReviewManager.tsx` +- [ ] `src/components/moderation/ValidationSummary.tsx` +- [ ] `src/components/ui/calendar.tsx` +- [ ] `src/hooks/useModerationStats.ts` +- [ ] `src/hooks/useUnitPreferences.ts` +- [ ] `src/lib/notificationService.ts` +- [ ] `src/lib/submissionItemsService.ts` +- [ ] `src/pages/Profile.tsx` +- [ ] Others... + +--- + +## 📊 Progress Tracker + +- ✅ Phase 1: Core Infrastructure (100%) +- ⏳ Phase 2: Catch Blocks (0/86) +- ⏳ Phase 3: Type Assertions (0/76) + +**Total Type Safety:** ~12% complete + +--- + +## 🎯 Next Steps + +1. Start with high-impact auth and moderation components +2. Replace catch blocks in batches of 10-15 files +3. Test each batch before proceeding +4. Address `as any` assertions systematically by category +5. Run TypeScript strict mode to catch remaining issues + +## 🚀 Benefits After Complete Migration + +- ✅ Zero runtime type errors from error handling +- ✅ Compile-time validation of all form submissions +- ✅ ESLint preventing new `any` usage +- ✅ Better IDE autocomplete and type hints +- ✅ Easier refactoring and maintenance +- ✅ Improved code documentation through types diff --git a/eslint.config.js b/eslint.config.js index 72bb8a9d..a9c5fa28 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,10 +22,10 @@ export default tseslint.config( "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-unsafe-assignment": "warn", - "@typescript-eslint/no-unsafe-member-access": "warn", - "@typescript-eslint/no-unsafe-call": "warn", - "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/no-unsafe-assignment": "error", + "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unsafe-call": "error", + "@typescript-eslint/no-unsafe-return": "error", }, }, ); diff --git a/src/components/admin/DesignerForm.tsx b/src/components/admin/DesignerForm.tsx index 6092f879..6a1bb78d 100644 --- a/src/components/admin/DesignerForm.tsx +++ b/src/components/admin/DesignerForm.tsx @@ -20,6 +20,7 @@ import { submitDesignerCreation, submitDesignerUpdate } from '@/lib/entitySubmis import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; +import type { UploadedImage } from '@/types/company'; // Raw form input state (before Zod transformation) interface DesignerFormInput { @@ -34,7 +35,7 @@ interface DesignerFormInput { headquarters_location?: string; website_url?: string; images?: { - uploaded: any[]; + uploaded: UploadedImage[]; banner_assignment?: number | null; card_assignment?: number | null; }; diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx index 7776313f..0a6a8b2e 100644 --- a/src/components/admin/ManufacturerForm.tsx +++ b/src/components/admin/ManufacturerForm.tsx @@ -21,6 +21,7 @@ import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; import { toDateOnly } from '@/lib/dateUtils'; +import type { UploadedImage } from '@/types/company'; // Raw form input state (before Zod transformation) interface ManufacturerFormInput { @@ -35,7 +36,7 @@ interface ManufacturerFormInput { headquarters_location?: string; website_url?: string; images?: { - uploaded: any[]; + uploaded: UploadedImage[]; banner_assignment?: number | null; card_assignment?: number | null; }; diff --git a/src/components/admin/OperatorForm.tsx b/src/components/admin/OperatorForm.tsx index 823d524e..68bbf9e7 100644 --- a/src/components/admin/OperatorForm.tsx +++ b/src/components/admin/OperatorForm.tsx @@ -20,6 +20,7 @@ import { submitOperatorCreation, submitOperatorUpdate } from '@/lib/entitySubmis import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; +import type { UploadedImage } from '@/types/company'; // Raw form input state (before Zod transformation) interface OperatorFormInput { @@ -34,7 +35,7 @@ interface OperatorFormInput { headquarters_location?: string; website_url?: string; images?: { - uploaded: any[]; + uploaded: UploadedImage[]; banner_assignment?: number | null; card_assignment?: number | null; }; diff --git a/src/components/admin/PropertyOwnerForm.tsx b/src/components/admin/PropertyOwnerForm.tsx index 0ac77d45..d55d76d6 100644 --- a/src/components/admin/PropertyOwnerForm.tsx +++ b/src/components/admin/PropertyOwnerForm.tsx @@ -20,6 +20,7 @@ import { submitPropertyOwnerCreation, submitPropertyOwnerUpdate } from '@/lib/en import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; +import type { UploadedImage } from '@/types/company'; // Raw form input state (before Zod transformation) interface PropertyOwnerFormInput { @@ -34,7 +35,7 @@ interface PropertyOwnerFormInput { headquarters_location?: string; website_url?: string; images?: { - uploaded: any[]; + uploaded: UploadedImage[]; banner_assignment?: number | null; card_assignment?: number | null; }; diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index e62c6364..6e234f84 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { validateSubmissionHandler } from '@/lib/entityFormValidation'; import type { RideTechnicalSpec, RideCoasterStat, RideNameHistory } from '@/types/database'; +import type { TempCompanyData, TempRideModelData } from '@/types/company'; import { entitySchemas } from '@/lib/entityValidationSchemas'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -129,8 +130,8 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: initialData?.manufacturer_id || '' ); const [selectedManufacturerName, setSelectedManufacturerName] = useState(''); - const [tempNewManufacturer, setTempNewManufacturer] = useState(null); - const [tempNewRideModel, setTempNewRideModel] = useState(null); + const [tempNewManufacturer, setTempNewManufacturer] = useState(null); + const [tempNewRideModel, setTempNewRideModel] = useState(null); const [isManufacturerModalOpen, setIsManufacturerModalOpen] = useState(false); const [isModelModalOpen, setIsModelModalOpen] = useState(false); diff --git a/src/lib/errorHandler.ts b/src/lib/errorHandler.ts index ebf83ef3..f8e1a0d1 100644 --- a/src/lib/errorHandler.ts +++ b/src/lib/errorHandler.ts @@ -61,3 +61,16 @@ export const handleInfo = ( duration: 4000 }); }; + +/** + * Type-safe error message extraction utility + * Use this instead of `error: any` in catch blocks + */ +export const getErrorMessage = (error: unknown): string => { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + if (error && typeof error === 'object' && 'message' in error) { + return String(error.message); + } + return 'An unexpected error occurred'; +}; diff --git a/src/types/company.ts b/src/types/company.ts index 19e2273e..60780dbf 100644 --- a/src/types/company.ts +++ b/src/types/company.ts @@ -4,6 +4,14 @@ import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; +export interface UploadedImage { + url: string; + cloudflare_id?: string; + file?: File; + isLocal?: boolean; + caption?: string; +} + export interface CompanyFormData { name: string; slug: string; @@ -26,3 +34,17 @@ export interface TempCompanyData { headquarters_location?: string; website_url?: string; } + +export interface TempRideModelData { + name: string; + slug: string; + category: string; + ride_type: string; + description?: string; + images?: { + uploaded: UploadedImage[]; + banner_assignment?: number | null; + card_assignment?: number | null; + }; + _technical_specifications?: unknown[]; +} diff --git a/src/types/submissions.ts b/src/types/submissions.ts index eed4e2b4..85c7bcdb 100644 --- a/src/types/submissions.ts +++ b/src/types/submissions.ts @@ -31,8 +31,8 @@ export interface SubmissionItemData { id: string; submission_id: string; item_type: EntityType | 'photo' | 'ride_model'; - item_data: any; - original_data?: any; + item_data: Record; + original_data?: Record; status: 'pending' | 'approved' | 'rejected'; depends_on: string | null; order_index: number; @@ -124,3 +124,4 @@ export function createSubmissionContent( ...referenceIds }; } +