fitaiProto/ACCEPTABLE_ANY_USAGE.md
2026-03-10 04:14:03 +01:00

7.3 KiB

Acceptable any Type Usage in FitAI

This document outlines the cases where any types are acceptable in the FitAI codebase.

Philosophy

While TypeScript's type safety is valuable, there are legitimate cases where any is the pragmatic choice:

  1. External library limitations - When third-party types are incomplete or incompatible
  2. Type system limitations - When TypeScript's type system cannot express the actual types
  3. Drizzle ORM constraints - Version mismatches and SQLite-specific type issues

Acceptable any Usage Categories

1. AG-Grid Callbacks (Admin App)

Location: apps/admin/src/components/users/UserGrid.tsx

Why: AG-Grid's TypeScript definitions use loose any types for cell renderers, value formatters, and other callbacks.

Example:

cellRenderer: (params: any) => {
  return params.value ? "Active" : "Inactive";
};

valueFormatter: (params: any) => {
  return params.value || "N/A";
};

Mitigation: None needed - AG-Grid's official types use any.

2. ECharts Callbacks (Admin App)

Location: apps/admin/src/components/charts/*.tsx

Why: ECharts (Apache ECharts) TypeScript types use any for formatter and renderer callbacks.

Example:

formatter: (params: any) => `$${params.value.toLocaleString()}`;

renderer: (params: any) => {
  return params.data.value > threshold ? "red" : "green";
};

Mitigation: None needed - ECharts' official types use any.

3. Drizzle ORM Type Assertions

Location: apps/admin/src/lib/database/drizzle.ts, apps/admin/src/lib/filtering.ts

Why: Multiple Drizzle ORM version mismatch causes SQL<unknown> type incompatibility. Drizzle's insert/update operations require type assertions when domain types don't exactly match database schema.

Example:

// Insert operations
await this.db.insert(clients).values(newClient as any);

// Update operations
await this.db
  .update(users)
  .set({ ...updateData, updatedAt: new Date() } as any)
  .where(eq(users.id, id));

// WHERE conditions (due to version mismatch)
const whereConditions: any[] = [];

// Column maps for filtering
columnMap: Record<string, any>; // SQLiteColumn type not compatible with Column type

Root Cause:

  • Two versions of drizzle-orm installed (one in packages/database, one in apps/admin)
  • SQL<unknown> from one package !== SQL<unknown> from another package
  • SQLite-specific column types don't match generic Column type

Mitigation:

  • ESLint comment added: // eslint-disable-line @typescript-eslint/no-explicit-any
  • Domain model mappers ensure type safety at the boundary
  • Consider consolidating Drizzle versions in future (requires monorepo restructuring)

4. Ionicons Type Assertions (Mobile App)

Location: apps/mobile/src/components/*.tsx, apps/mobile/src/app/*.tsx

Why: Ionicons icon names are strings, but the component expects specific literal types.

Example:

<Ionicons name={icon as any} size={24} color={theme.colors.primary} />

Mitigation: Icon name constants could be typed more strictly, but this is low priority.

5. Fitness Profile Update Field (Mobile App)

Location: apps/mobile/src/app/fitness-profile.tsx

Why: Generic update function handles multiple field types (string, number, array, enum).

Example:

const updateField = (field: keyof FitnessProfileData, value: any) => {
  setProfile((prev) => ({ ...prev, [field]: value }));
};

Mitigation: Could use generics, but current approach is simple and type-safe at call sites.

6. Fitness Goals Service Headers (Mobile App)

Location: apps/mobile/src/services/fitnessGoals.ts

Why: Auth headers may or may not include Authorization based on token availability.

Example:

private async getAuthHeaders(token: string | null): Promise<any> {
  const headers: any = {
    'Content-Type': 'application/json',
  };
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }
  return headers;
}

Mitigation: Could use Record<string, string> but any is acceptable for headers.

Eliminated any Usage

What We Fixed in Phase 5

  1. Error catch blocks - Changed from catch (error: any) to catch (error: unknown) with type-safe error message extraction
  2. Clerk metadata access - Created type guards (getGymIdFromUser, getGymIdFromMetadata)
  3. Database row mappers - Created specific row type interfaces (UserRow, ClientRow, etc.)
  4. AI prompt builder - Changed profile: any to profile: FitnessProfile
  5. User role validation - Proper type guards instead of any return type
  6. Recommendation types - Added missing fields to interface

Files Modified

Admin App (9 files):

  • src/lib/error-helpers.ts (NEW)
  • src/app/api/invitations/route.ts
  • src/app/api/admin/set-user-metadata/route.ts
  • src/components/users/UserManagement.tsx
  • src/lib/database/drizzle.ts
  • src/lib/ai/prompt-builder.ts
  • src/lib/sync-user.ts
  • src/app/recommendations/page.tsx
  • src/lib/filtering.ts

Mobile App (6 files):

  • src/utils/error-helpers.ts (NEW)
  • src/app/welcome.tsx
  • src/app/(auth)/sign-in.tsx
  • src/app/(auth)/sign-up.tsx
  • src/api/fitnessProfile.ts
  • src/app/(tabs)/attendance.tsx

Type Safety Improvements

Error Handling

Before:

catch (error: any) {
  Alert.alert('Error', error.response?.data?.error || 'Failed');
}

After:

catch (error: unknown) {
  Alert.alert('Error', getErrorMessage(error, 'Failed'));
}

Clerk Metadata

Before:

gymId: String((user?.publicMetadata as any)?.gymId ?? "");

After:

gymId: user ? getGymIdFromUser(user) : "";

Database Mappers

Before:

function mapUser(row: any): User {
  // ...
}

After:

interface UserRow extends Record<string, unknown> {
  id: string;
  email: string;
  // ... all fields
}

function mapUser(row: UserRow): User {
  // ...
}

Remaining any Count

After Phase 5:

  • Admin App: ~40 instances (down from 72)
    • 30 in AG-Grid/ECharts callbacks (acceptable)
    • 10 in Drizzle operations (acceptable due to type system limitations)
  • Mobile App: ~8 instances (down from 24)
    • 4 in Ionicons (acceptable)
    • 2 in headers/generic functions (acceptable)
    • 2 in component callbacks (acceptable)

Best Practices Going Forward

  1. Never use any for error catch blocks - Always use unknown and getErrorMessage()
  2. Never use any for user inputs - Always validate and type properly
  3. Document why any is used - Add comment explaining the limitation
  4. Use ESLint disable sparingly - Only for known acceptable cases
  5. Prefer unknown over any - Force explicit type checking

ESLint Configuration

The admin app's ESLint warns on @typescript-eslint/no-explicit-any but doesn't error. This allows flexibility while encouraging proper typing.

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn"
  }
}

Conclusion

All any types remaining in the codebase are either:

  1. Required by external library types (AG-Grid, ECharts)
  2. Necessary due to Drizzle ORM limitations
  3. Low-impact utility code

Critical paths (API routes, database operations, error handling, authentication) are now fully type-safe.