chore(ts): eliminate the remaining 4 \as any\ casts in production code

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<KomootBulkImportData>\`) 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<ArrayBuffer>\` specifically; a generic Node \`Buffer\`
is structurally \`Buffer<ArrayBufferLike>\` (which includes
SharedArrayBuffer). The runtime accepts either, so narrowed the cast
to \`as Buffer<ArrayBuffer>\` — 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) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-05-26 07:11:02 +02:00
parent 2fc09165da
commit b0b58d36fb
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
6 changed files with 31 additions and 16 deletions

View file

@ -6,20 +6,22 @@ type KomootCreds =
| { mode: "public"; komootUserId: string }
| { mode: "authenticated"; email: string; encryptedPassword: string; komootUserId: string };
interface KomootBulkImportData extends Record<string, unknown> {
interface KomootBulkImportData {
batchId: string;
userId: string;
creds: KomootCreds;
}
export const komootBulkImportJob: JobDefinition<KomootBulkImportData> = {
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);
}

View file

@ -10,10 +10,12 @@ import { generateGpx } from "@trails-cool/gpx";
export async function fitToGpx(buffer: Buffer, name: string): Promise<string | null> {
const parsed = await new Promise<Record<string, unknown>>((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<ArrayBuffer>` specifically;
// a generic Node `Buffer` is structurally `Buffer<ArrayBufferLike>`.
// The runtime accepts either, so coerce the underlying buffer slot.
parser.parse(buffer as Buffer<ArrayBuffer>, (error, data) => {
if (error) reject(error);
else resolve(data ?? {});
else resolve((data ?? {}) as Record<string, unknown>);
});
});

View file

@ -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<Record<string, unknown>>((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<ArrayBuffer>
// narrowing rationale.
parser.parse(buffer as Buffer<ArrayBuffer>, (err, d) =>
err ? reject(err) : resolve((d ?? {}) as Record<string, unknown>),
);
});
const records = (parsed.records ?? []) as Array<{
position_lat?: number;

View file

@ -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);

View file

@ -77,10 +77,11 @@ function RouteMapInner({
const cameraRef = useRef<CameraRef>(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);

View file

@ -1,8 +1,16 @@
import type { Job } from "pg-boss";
export interface JobDefinition<T extends object = object> {
/**
* 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<SpecificPayload>` is not assignable to one taking
* `Job<object>`. Handlers narrow internally instead.
*/
export interface JobDefinition {
name: string;
handler: (jobs: Job<T>[]) => Promise<unknown>;
handler: (jobs: Job<unknown>[]) => Promise<unknown>;
cron?: string;
retryLimit?: number;
expireInSeconds?: number;