From 985ec5402322a1ab34dff498b4e5fc881c2bbad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Tue, 26 May 2026 00:57:18 +0200 Subject: [PATCH 1/2] chore(ts): replace 7 \`as unknown as\` shims with proper types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of the existing coercions are legitimate (Drizzle raw-SQL row shapes, linkedom DOMParser interop, fit-file-parser's loose \`any\` callback API) — leaving those is honest. The ones cleaned up here were shims around APIs that have real types we just hadn't wired: - **\`window.__leafletMap\`** (4 sites: MapHelpers.tsx, SessionView.tsx ×3) Added \`Window\` declaration in \`apps/planner/app/types/global.d.ts\`. Callers now use plain \`window.__leafletMap\` typed as \`L.Map | undefined\`. - **\`L.markerClusterGroup\`** (PoiPanel.tsx) — leaflet.markercluster augments the global \`L\` namespace at runtime but ships no types. Added a \`declare module \"leaflet\"\` block in the same global.d.ts with a minimal signature for what we actually use. - **\`L.DomEvent.preventDefault / .stop\`** taking a Leaflet event rather than a native one (PlannerMap.tsx, RouteInteraction.tsx, NoGoAreaLayer.tsx). Leaflet events carry the native event under \`.originalEvent\`; using that drops the cast entirely. - **\`_creds as unknown as OAuthCredentials\`** orphan in wahoo/webhook.ts — \`_creds\` was unused inside the callback (the void-cast was a no-op preserving the type for documentation). Replaced with \`async ()\`, dropped the unused import. Net: 26 \`as unknown as\` / \`as any\` sites → 19, all remaining ones gated by external lib interop or Drizzle raw-SQL. Full repo: pnpm typecheck / lint / test all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../providers/wahoo/webhook.ts | 4 +-- apps/planner/app/components/MapHelpers.tsx | 2 +- apps/planner/app/components/NoGoAreaLayer.tsx | 2 +- apps/planner/app/components/PlannerMap.tsx | 2 +- apps/planner/app/components/PoiPanel.tsx | 5 +-- .../app/components/RouteInteraction.tsx | 2 +- apps/planner/app/components/SessionView.tsx | 6 ++-- apps/planner/app/types/global.d.ts | 34 +++++++++++++++++++ 8 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 apps/planner/app/types/global.d.ts diff --git a/apps/journal/app/lib/connected-services/providers/wahoo/webhook.ts b/apps/journal/app/lib/connected-services/providers/wahoo/webhook.ts index ba6b664..6473b0b 100644 --- a/apps/journal/app/lib/connected-services/providers/wahoo/webhook.ts +++ b/apps/journal/app/lib/connected-services/providers/wahoo/webhook.ts @@ -9,7 +9,6 @@ import { fitToGpx } from "../../fit.ts"; import { fetchWithTimeout } from "../../../http.server.ts"; import { isAlreadyImported, importActivity } from "../../../sync/imports.server.ts"; import { getServiceByProviderUser, withFreshCredentials } from "../../manager.ts"; -import type { OAuthCredentials } from "../../types.ts"; import type { WebhookEvent, WebhookReceiver } from "../../registry.ts"; interface WahooWebhookBody { @@ -52,8 +51,7 @@ export const wahooWebhook: WebhookReceiver = { // through withFreshCredentials so the manager has a chance to refresh // a near-expired credential before any subsequent Wahoo call this // handler might make. - const buffer = await withFreshCredentials(service.id, async (_creds) => { - void (_creds as unknown as OAuthCredentials); + const buffer = await withFreshCredentials(service.id, async () => { const resp = await fetchWithTimeout(event.fileUrl!); if (!resp.ok) throw new Error(`Wahoo file download failed: ${resp.status}`); return Buffer.from(await resp.arrayBuffer()); diff --git a/apps/planner/app/components/MapHelpers.tsx b/apps/planner/app/components/MapHelpers.tsx index 73753d4..e75ba34 100644 --- a/apps/planner/app/components/MapHelpers.tsx +++ b/apps/planner/app/components/MapHelpers.tsx @@ -11,7 +11,7 @@ export function MapExposer() { const map = useMap(); useEffect(() => { if (typeof window !== "undefined") { - (window as unknown as Record).__leafletMap = map; + window.__leafletMap = map; } }, [map]); return null; diff --git a/apps/planner/app/components/NoGoAreaLayer.tsx b/apps/planner/app/components/NoGoAreaLayer.tsx index b6508ee..9179890 100644 --- a/apps/planner/app/components/NoGoAreaLayer.tsx +++ b/apps/planner/app/components/NoGoAreaLayer.tsx @@ -45,7 +45,7 @@ export function NoGoAreaLayer({ noGoAreas, doc, enabled, onToggle }: NoGoAreaLay const polygon = L.polygon(latLngs, NO_GO_STYLE); polygon.on("contextmenu", (e) => { - L.DomEvent.preventDefault(e as unknown as Event); + L.DomEvent.preventDefault(e.originalEvent); suppressObserverRef.current = true; doc.transact(() => noGoAreas.delete(i, 1), "local"); suppressObserverRef.current = false; diff --git a/apps/planner/app/components/PlannerMap.tsx b/apps/planner/app/components/PlannerMap.tsx index 76c9de5..0a135d3 100644 --- a/apps/planner/app/components/PlannerMap.tsx +++ b/apps/planner/app/components/PlannerMap.tsx @@ -210,7 +210,7 @@ export function PlannerMap({ yjs, sessionId, onRouteRequest, highlightPosition, moveWaypoint(i, lat, lng); }, contextmenu: (e) => { - L.DomEvent.preventDefault(e as unknown as Event); + L.DomEvent.preventDefault(e.originalEvent); const orig = e.originalEvent as MouseEvent; setContextMenu({ x: orig.clientX, y: orig.clientY, index: i }); }, diff --git a/apps/planner/app/components/PoiPanel.tsx b/apps/planner/app/components/PoiPanel.tsx index 1f55dc0..9c31617 100644 --- a/apps/planner/app/components/PoiPanel.tsx +++ b/apps/planner/app/components/PoiPanel.tsx @@ -90,8 +90,9 @@ export function PoiMarkers({ poiState, onAddWaypoint }: PoiPanelProps) { // Dynamic import to avoid bundling markercluster when POIs aren't used import("leaflet.markercluster").then(() => { if (!mounted) return; - // After import, L.markerClusterGroup is available - const cluster = (L as unknown as { markerClusterGroup: (opts?: object) => L.LayerGroup }).markerClusterGroup({ + // After import, L.markerClusterGroup is available (typed via + // module augmentation in app/types/global.d.ts). + const cluster = L.markerClusterGroup({ maxClusterRadius: 40, disableClusteringAtZoom: 15, showCoverageOnHover: false, diff --git a/apps/planner/app/components/RouteInteraction.tsx b/apps/planner/app/components/RouteInteraction.tsx index 36375f9..205da20 100644 --- a/apps/planner/app/components/RouteInteraction.tsx +++ b/apps/planner/app/components/RouteInteraction.tsx @@ -194,7 +194,7 @@ export function RouteInteraction({ // Prevent double-click zoom when clicking ghost marker marker.on("dblclick", (e) => { - L.DomEvent.stop(e as unknown as Event); + L.DomEvent.stop(e.originalEvent); }); map.on("mousemove", onMouseMove); diff --git a/apps/planner/app/components/SessionView.tsx b/apps/planner/app/components/SessionView.tsx index c393489..7a9e170 100644 --- a/apps/planner/app/components/SessionView.tsx +++ b/apps/planner/app/components/SessionView.tsx @@ -195,18 +195,18 @@ export function SessionView({ sessionId, hasJournalCallback, returnUrl, initialW }, []); const handleChartClick = useCallback((position: [number, number]) => { - const map = (window as unknown as Record).__leafletMap as L.Map | undefined; + const map = window.__leafletMap; map?.panTo(position); }, []); const handleChartDragSelect = useCallback((bounds: [[number, number], [number, number]]) => { - const map = (window as unknown as Record).__leafletMap as L.Map | undefined; + const map = window.__leafletMap; map?.fitBounds(bounds, { padding: [30, 30] }); setIsZoomedByChart(true); }, []); const handleResetZoom = useCallback(() => { - const map = (window as unknown as Record).__leafletMap as L.Map | undefined; + const map = window.__leafletMap; if (!map || !yjs) return; const coordsJson = yjs.routeData.get("coordinates") as string | undefined; if (!coordsJson) return; diff --git a/apps/planner/app/types/global.d.ts b/apps/planner/app/types/global.d.ts new file mode 100644 index 0000000..3b9e179 --- /dev/null +++ b/apps/planner/app/types/global.d.ts @@ -0,0 +1,34 @@ +// Ambient type augmentations for the planner client. +// +// Picked up by tsconfig's default `include` (every `**/*.ts(x)` inside +// the project root). Keeps the `as unknown as Record` +// dance out of every site that needs to read or write a globally- +// exposed value. + +import type { Map as LeafletMap, LayerGroup } from "leaflet"; + +declare global { + // Exposed by MapHelpers.tsx::MapExposer for E2E tests and external + // integrations. Optional because it's only present after the map + // mounts. + interface Window { + __leafletMap?: LeafletMap; + } +} + +// Module augmentation for leaflet.markercluster. The plugin extends +// the global `L` namespace at runtime but ships no TypeScript types, +// so without this declaration callers fall back to `as unknown as +// {...}` shims. +declare module "leaflet" { + function markerClusterGroup(options?: MarkerClusterGroupOptions): LayerGroup; + interface MarkerClusterGroupOptions { + showCoverageOnHover?: boolean; + spiderfyOnMaxZoom?: boolean; + zoomToBoundsOnClick?: boolean; + maxClusterRadius?: number; + [key: string]: unknown; + } +} + +export {}; From a724f862b8f318ca575e71c3a5d05f78137522d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Tue, 26 May 2026 01:02:49 +0200 Subject: [PATCH 2/2] chore(ts): drop two more redundant casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RouteMapThumbnail.client.tsx: `{ type: 'Feature', ... } as unknown as GeoJsonObject` → `as GeoJsonObject`. The literal already satisfies the type; the unknown bridge was unnecessary. - use-yjs.ts:102: keep the coercion (y-websocket's `.on()` signature doesn't include the legacy `"synced"` event even though the runtime still fires it), but document why with a comment so future readers don't try to drop it. --- apps/journal/app/components/RouteMapThumbnail.client.tsx | 2 +- apps/planner/app/lib/use-yjs.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/journal/app/components/RouteMapThumbnail.client.tsx b/apps/journal/app/components/RouteMapThumbnail.client.tsx index 3ea476d..5a4591d 100644 --- a/apps/journal/app/components/RouteMapThumbnail.client.tsx +++ b/apps/journal/app/components/RouteMapThumbnail.client.tsx @@ -142,7 +142,7 @@ function DayColoredRoute({ data, dayBreaks, highlightedDay }: { data: GeoJsonObj type: "Feature", geometry: { type: "LineString", coordinates: seg.coords }, properties: {}, - } as unknown as GeoJsonObject; + } as GeoJsonObject; return ( void): void }).on("synced", () => { // Only add if the doc is empty (avoid duplicating on reconnect) if (waypoints.length === 0 && !initializedWaypoints.current) {