From b0b58d36fb6a3f21225c9286dc5ffff09fb82546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Tue, 26 May 2026 07:11:02 +0200 Subject: [PATCH] chore(ts): eliminate the remaining 4 \`as any\` casts in production code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Followup to #447. The audit ran on \`as unknown as\` first; this PR closes out \`as any\` separately. After this, \`grep -rn ' as any\\b' apps/ packages/\` returns 0 (excluding tests and node_modules). ## Sites fixed **\`apps/journal/server.ts\`** + **\`packages/jobs/src/types.ts\`** — \`komootBulkImportJob as any\`. The job had a typed payload (\`JobDefinition\`) but the worker's \`JobDefinition[]\` array forced a contravariance cast at every site that mixed typed and untyped jobs. Dropped the generic from \`JobDefinition\` entirely; handlers narrow their own \`job.data\`. Only one job (komoot-bulk-import) used the generic, so the surface is tiny. **\`apps/journal/app/lib/connected-services/fit.ts\`** + same pattern in \`routes/sync.import.\$provider.server.ts\` — \`parser.parse(buffer as any, (err: unknown, d: any) => ...)\`. fit-file-parser's TypeScript types require \`Buffer\` specifically; a generic Node \`Buffer\` is structurally \`Buffer\` (which includes SharedArrayBuffer). The runtime accepts either, so narrowed the cast to \`as Buffer\` — still a cast, but precise about what we're asserting and why. Removed the \`(err, d: any) => …\` ad-hoc callback typings; the library exports a proper \`FitParserCallback\`. **\`apps/mobile/lib/editor/RouteMap.tsx\`** — \`onLongPress\` handler took \`(event: any)\`. maplibre-react-native v11 generates the event type via React Native codegen but doesn't re-export it as a public TypeScript type. Replaced with a local structural slice of the parts we actually read. Net: 4 \`as any\` sites in prod code → 0. \`pnpm typecheck\` / \`lint\` / \`test\` all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/journal/app/jobs/komoot-bulk-import.ts | 8 +++++--- apps/journal/app/lib/connected-services/fit.ts | 8 +++++--- .../app/routes/sync.import.$provider.server.ts | 7 +++++-- apps/journal/server.ts | 3 +-- apps/mobile/lib/editor/RouteMap.tsx | 9 +++++---- packages/jobs/src/types.ts | 12 ++++++++++-- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/apps/journal/app/jobs/komoot-bulk-import.ts b/apps/journal/app/jobs/komoot-bulk-import.ts index 84c4bc8..87d788d 100644 --- a/apps/journal/app/jobs/komoot-bulk-import.ts +++ b/apps/journal/app/jobs/komoot-bulk-import.ts @@ -6,20 +6,22 @@ type KomootCreds = | { mode: "public"; komootUserId: string } | { mode: "authenticated"; email: string; encryptedPassword: string; komootUserId: string }; -interface KomootBulkImportData extends Record { +interface KomootBulkImportData { batchId: string; userId: string; creds: KomootCreds; } -export const komootBulkImportJob: JobDefinition = { +export const komootBulkImportJob: JobDefinition = { name: "komoot-bulk-import", retryLimit: 1, expireInSeconds: 1800, async handler(jobs) { const batch = Array.isArray(jobs) ? jobs : [jobs]; for (const job of batch) { - const { batchId, userId, creds } = job.data; + // pg-boss serialized payload — caller (enqueueOptional) wrote it + // as KomootBulkImportData. Narrow at the boundary. + const { batchId, userId, creds } = job.data as KomootBulkImportData; logger.info({ batchId, userId }, "komoot bulk import job started"); await runKomootBulkImport(batchId, userId, creds); } diff --git a/apps/journal/app/lib/connected-services/fit.ts b/apps/journal/app/lib/connected-services/fit.ts index 64f1f65..b928acf 100644 --- a/apps/journal/app/lib/connected-services/fit.ts +++ b/apps/journal/app/lib/connected-services/fit.ts @@ -10,10 +10,12 @@ import { generateGpx } from "@trails-cool/gpx"; export async function fitToGpx(buffer: Buffer, name: string): Promise { const parsed = await new Promise>((resolve, reject) => { const parser = new FitParser({ force: true }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parser.parse(buffer as any, (error: unknown, data: any) => { + // fit-file-parser's typing requires `Buffer` specifically; + // a generic Node `Buffer` is structurally `Buffer`. + // The runtime accepts either, so coerce the underlying buffer slot. + parser.parse(buffer as Buffer, (error, data) => { if (error) reject(error); - else resolve(data ?? {}); + else resolve((data ?? {}) as Record); }); }); diff --git a/apps/journal/app/routes/sync.import.$provider.server.ts b/apps/journal/app/routes/sync.import.$provider.server.ts index 7b51104..da973b2 100644 --- a/apps/journal/app/routes/sync.import.$provider.server.ts +++ b/apps/journal/app/routes/sync.import.$provider.server.ts @@ -85,8 +85,11 @@ export async function syncImportProviderAction(request: Request, provider: strin const { default: FitParser } = await import("fit-file-parser"); const parsed = await new Promise>((resolve, reject) => { const parser = new FitParser({ force: true }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parser.parse(buffer as any, (err: unknown, d: any) => (err ? reject(err) : resolve(d ?? {}))); + // See lib/connected-services/fit.ts for the same Buffer + // narrowing rationale. + parser.parse(buffer as Buffer, (err, d) => + err ? reject(err) : resolve((d ?? {}) as Record), + ); }); const records = (parsed.records ?? []) as Array<{ position_lat?: number; diff --git a/apps/journal/server.ts b/apps/journal/server.ts index a140502..38b8751 100644 --- a/apps/journal/server.ts +++ b/apps/journal/server.ts @@ -177,8 +177,7 @@ server.listen(port, async () => { const { importBatchesSweepJob } = await import("./app/jobs/import-batches-sweep.ts"); const { sendWelcomeEmailJob } = await import("./app/jobs/send-welcome-email.ts"); const { consumedJtiSweepJob } = await import("./app/jobs/consumed-jti-sweep.ts"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - jobs.push(notificationsFanoutJob, notificationsPurgeJob, komootBulkImportJob as any, importBatchesSweepJob, sendWelcomeEmailJob, consumedJtiSweepJob); + jobs.push(notificationsFanoutJob, notificationsPurgeJob, komootBulkImportJob, importBatchesSweepJob, sendWelcomeEmailJob, consumedJtiSweepJob); const boss = createBoss(getDatabaseUrl()); await startWorker(boss, jobs); diff --git a/apps/mobile/lib/editor/RouteMap.tsx b/apps/mobile/lib/editor/RouteMap.tsx index afcd600..d627860 100644 --- a/apps/mobile/lib/editor/RouteMap.tsx +++ b/apps/mobile/lib/editor/RouteMap.tsx @@ -77,10 +77,11 @@ function RouteMapInner({ const cameraRef = useRef(null); const handleLongPress = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (event: any) => { - // v11 event shape: payload lives on `event.nativeEvent.lngLat` - // (the old `event.geometry.coordinates` is gone). + // maplibre-react-native v11+ codegen `NativePressEvent` isn't + // re-exported as a type, so we declare the slice we use locally. + // Payload moved from `event.geometry.coordinates` (v10) to + // `event.nativeEvent.lngLat` in v11. + (event: { nativeEvent?: { lngLat?: readonly [number, number] } }) => { const lngLat = event?.nativeEvent?.lngLat; if (Array.isArray(lngLat) && lngLat.length >= 2) { onLongPress(lngLat[1] as number, lngLat[0] as number); diff --git a/packages/jobs/src/types.ts b/packages/jobs/src/types.ts index 0dc923e..3f75205 100644 --- a/packages/jobs/src/types.ts +++ b/packages/jobs/src/types.ts @@ -1,8 +1,16 @@ import type { Job } from "pg-boss"; -export interface JobDefinition { +/** + * A pg-boss job definition. The payload type is intentionally + * `unknown` at this boundary: a heterogeneous `JobDefinition[]` array + * (e.g. the one in `apps/journal/server.ts`) was previously forced to + * cast each typed job to `any` because of contravariance — a handler + * taking `Job` is not assignable to one taking + * `Job`. Handlers narrow internally instead. + */ +export interface JobDefinition { name: string; - handler: (jobs: Job[]) => Promise; + handler: (jobs: Job[]) => Promise; cron?: string; retryLimit?: number; expireInSeconds?: number;