From 75d6948a81164dc80e33d41680d2c343b482ac73 Mon Sep 17 00:00:00 2001 From: Uruk Date: Fri, 14 Nov 2025 23:48:59 +0100 Subject: [PATCH] refactor: replace `any` types with proper TypeScript types Improves type safety throughout the codebase by eliminating unsafe `any` type assertions and replacing them with proper type definitions. Adds explicit type parameters and constraints to MMKV augmentations, component props, and router navigation calls. Updates function signatures to use `unknown` instead of `any` where appropriate, and properly types Icon glyphs, router Href parameters, and component prop spreads. Enhances maintainability and catches potential type errors at compile time rather than runtime. --- augmentations/mmkv.ts | 11 ++++---- components/BitRateSheet.tsx | 4 +-- components/RoundButton.tsx | 26 ++++++++++++++----- components/TrackSheet.tsx | 8 +++--- .../apple-tv-carousel/AppleTVCarousel.tsx | 4 +-- components/common/TouchableItemRouter.tsx | 6 ++--- components/home/LargeMovieCarousel.tsx | 4 +-- .../jellyseerr/discover/CompanySlide.tsx | 8 +++--- components/jellyseerr/discover/GenreSlide.tsx | 8 +++--- components/list/ListGroup.tsx | 19 +++++++------- components/list/ListItem.tsx | 15 +++++++++-- components/video-player/controls/Controls.tsx | 4 +-- .../video-player/controls/GestureOverlay.tsx | 8 +++--- .../controls/contexts/VideoContext.tsx | 4 +-- .../controls/dropdown/DropdownView.tsx | 4 +-- utils/atoms/settings.ts | 5 ++-- 16 files changed, 81 insertions(+), 57 deletions(-) diff --git a/augmentations/mmkv.ts b/augmentations/mmkv.ts index d633f36e..885b743d 100644 --- a/augmentations/mmkv.ts +++ b/augmentations/mmkv.ts @@ -1,15 +1,16 @@ +import { MMKV } from "react-native-mmkv"; import { storage } from "@/utils/mmkv"; declare module "react-native-mmkv" { interface MMKV { get(key: string): T | undefined; - setAny(key: string, value: any | undefined): void; + setAny(key: string, value: unknown): void; } } -// Add the augmentation methods directly to the MMKV prototype -// This follows the recommended pattern while adding the helper methods your app uses -(storage as any).get = function (key: string): T | undefined { +// Add the augmentation methods directly to the MMKV instance +// We need to bind these methods to preserve the 'this' context +storage.get = function (this: MMKV, key: string): T | undefined { try { const serializedItem = this.getString(key); if (!serializedItem) return undefined; @@ -20,7 +21,7 @@ declare module "react-native-mmkv" { } }; -(storage as any).setAny = function (key: string, value: any | undefined): void { +storage.setAny = function (this: MMKV, key: string, value: unknown): void { try { if (value === undefined) { this.remove(key); diff --git a/components/BitRateSheet.tsx b/components/BitRateSheet.tsx index 0589cd05..fe6528f8 100644 --- a/components/BitRateSheet.tsx +++ b/components/BitRateSheet.tsx @@ -108,10 +108,10 @@ export const BitrateSheet: React.FC = ({ values={selected ? [selected] : []} multiple={false} searchFilter={(item, query) => { - const label = (item as any).key || ""; + const label = item.key || ""; return label.toLowerCase().includes(query.toLowerCase()); }} - renderItemLabel={(item) => {(item as any).key || ""}} + renderItemLabel={(item) => {item.key || ""}} set={(vals) => { const chosen = vals[0] as Bitrate | undefined; if (chosen) onChange(chosen); diff --git a/components/RoundButton.tsx b/components/RoundButton.tsx index 5d490f19..fb03ba56 100644 --- a/components/RoundButton.tsx +++ b/components/RoundButton.tsx @@ -4,7 +4,19 @@ import type { PropsWithChildren } from "react"; import { Platform, TouchableOpacity, type ViewProps } from "react-native"; import { useHaptic } from "@/hooks/useHaptic"; -interface Props extends ViewProps { +interface Props + extends Omit< + ViewProps, + | "children" + | "onPressIn" + | "onPressOut" + | "onPress" + | "nextFocusDown" + | "nextFocusForward" + | "nextFocusLeft" + | "nextFocusRight" + | "nextFocusUp" + > { onPress?: () => void; icon?: keyof typeof Ionicons.glyphMap; background?: boolean; @@ -41,7 +53,7 @@ export const RoundButton: React.FC> = ({ {icon ? ( > = ({ {icon ? ( > = ({ {icon ? ( > = ({ className={`rounded-full ${buttonSize} flex items-center justify-center ${ fillColor ? fillColorClass : "bg-transparent" }`} - {...(viewProps as any)} + {...viewProps} > {icon ? ( > = ({ ); return ( - + {icon ? ( = ({ } multiple={false} searchFilter={(item, query) => { - const label = (item as any).DisplayTitle || ""; + const label = item.DisplayTitle || ""; return label.toLowerCase().includes(query.toLowerCase()); }} - renderItemLabel={(item) => ( - {(item as any).DisplayTitle || ""} - )} + renderItemLabel={(item) => {item.DisplayTitle || ""}} set={(vals) => { - const chosen = vals[0] as any; + const chosen = vals[0]; if (chosen && chosen.Index !== null && chosen.Index !== undefined) { onChange(chosen.Index); } diff --git a/components/apple-tv-carousel/AppleTVCarousel.tsx b/components/apple-tv-carousel/AppleTVCarousel.tsx index c30711e8..b00616d0 100644 --- a/components/apple-tv-carousel/AppleTVCarousel.tsx +++ b/components/apple-tv-carousel/AppleTVCarousel.tsx @@ -7,7 +7,7 @@ import { import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; -import { useRouter } from "expo-router"; +import { type Href, useRouter } from "expo-router"; import { useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo, useState } from "react"; import { @@ -340,7 +340,7 @@ export const AppleTVCarousel: React.FC = ({ const navigateToItem = useCallback( (item: BaseItemDto) => { const navigation = getItemNavigation(item, "(home)"); - router.push(navigation as any); + router.push(navigation as Href); }, [router], ); diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index e95fd5e7..9fb9b1bb 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,6 +1,6 @@ import { useActionSheet } from "@expo/react-native-action-sheet"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { useRouter, useSegments } from "expo-router"; +import { type Href, useRouter, useSegments } from "expo-router"; import { type PropsWithChildren, useCallback } from "react"; import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; import { useFavorite } from "@/hooks/useFavorite"; @@ -146,12 +146,12 @@ export const TouchableItemRouter: React.FC> = ({ if (isOffline) { // For offline mode, we still need to use query params const url = `${itemRouter(item, from)}&offline=true`; - router.push(url as any); + router.push(url as Href); return; } const navigation = getItemNavigation(item, from); - router.push(navigation as any); + router.push(navigation as Href); }} {...props} > diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index 913e024b..07c024f9 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; -import { useRouter, useSegments } from "expo-router"; +import { type Href, useRouter, useSegments } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo } from "react"; import { Dimensions, View, type ViewProps } from "react-native"; @@ -156,7 +156,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => { if (!from) return; lightHapticFeedback(); const navigation = getItemNavigation(item, from); - router.push(navigation as any); + router.push(navigation as Href); }, [item, from]); const tap = Gesture.Tap() diff --git a/components/jellyseerr/discover/CompanySlide.tsx b/components/jellyseerr/discover/CompanySlide.tsx index 9643f48e..5a4461b0 100644 --- a/components/jellyseerr/discover/CompanySlide.tsx +++ b/components/jellyseerr/discover/CompanySlide.tsx @@ -1,4 +1,4 @@ -import { router, useSegments } from "expo-router"; +import { type Href, router, useSegments } from "expo-router"; import type React from "react"; import { useCallback } from "react"; import { TouchableOpacity, type ViewProps } from "react-native"; @@ -21,10 +21,10 @@ const CompanySlide: React.FC< const navigate = useCallback( ({ id, image, name }: Network | Studio) => router.push({ - pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}` as any, + pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`, params: { id, image, name, type: slide.type }, - }), - [slide], + } as Href), + [slide, from], ); return ( diff --git a/components/jellyseerr/discover/GenreSlide.tsx b/components/jellyseerr/discover/GenreSlide.tsx index 8edaf4c3..98f8a07c 100644 --- a/components/jellyseerr/discover/GenreSlide.tsx +++ b/components/jellyseerr/discover/GenreSlide.tsx @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { router, useSegments } from "expo-router"; +import { type Href, router, useSegments } from "expo-router"; import type React from "react"; import { useCallback } from "react"; import { TouchableOpacity, type ViewProps } from "react-native"; @@ -18,10 +18,10 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { const navigate = useCallback( (genre: GenreSliderItem) => router.push({ - pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}` as any, + pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`, params: { type: slide.type, name: genre.name }, - }), - [slide], + } as Href), + [slide, from], ); const { data } = useQuery({ diff --git a/components/list/ListGroup.tsx b/components/list/ListGroup.tsx index b9752bac..3259e969 100644 --- a/components/list/ListGroup.tsx +++ b/components/list/ListGroup.tsx @@ -31,15 +31,16 @@ export const ListGroup: React.FC> = ({ className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900' > {Children.map(childrenArray, (child, index) => { - if (isValidElement<{ style?: ViewStyle }>(child)) { - return cloneElement(child as any, { - style: StyleSheet.compose( - child.props.style, - index < childrenArray.length - 1 - ? styles.borderBottom - : undefined, - ), - }); + if (isValidElement(child)) { + const style = StyleSheet.compose( + (child.props as { style?: ViewStyle }).style, + index < childrenArray.length - 1 + ? styles.borderBottom + : undefined, + ); + return cloneElement(child, { style } as Partial< + typeof child.props + >); } return child; })} diff --git a/components/list/ListItem.tsx b/components/list/ListItem.tsx index 7ce33986..26096fbd 100644 --- a/components/list/ListItem.tsx +++ b/components/list/ListItem.tsx @@ -3,7 +3,18 @@ import type { PropsWithChildren, ReactNode } from "react"; import { TouchableOpacity, View, type ViewProps } from "react-native"; import { Text } from "../common/Text"; -interface Props extends ViewProps { +interface Props + extends Omit< + ViewProps, + | "children" + | "onPressIn" + | "onPressOut" + | "nextFocusDown" + | "nextFocusForward" + | "nextFocusLeft" + | "nextFocusRight" + | "nextFocusUp" + > { title?: string | null | undefined; subtitle?: string | null | undefined; value?: string | null | undefined; @@ -37,7 +48,7 @@ export const ListItem: React.FC> = ({ className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${ disabled ? "opacity-50" : "" }`} - {...(viewProps as any)} + {...viewProps} > = ({ console.log("queryParams", queryParams); - router.replace(`player/direct-player?${queryParams}` as any); + router.replace(`player/direct-player?${queryParams}` as Href); }, [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router], ); diff --git a/components/video-player/controls/GestureOverlay.tsx b/components/video-player/controls/GestureOverlay.tsx index e4ca20e6..936020e0 100644 --- a/components/video-player/controls/GestureOverlay.tsx +++ b/components/video-player/controls/GestureOverlay.tsx @@ -18,7 +18,7 @@ interface Props { interface FeedbackState { visible: boolean; - icon: string; + icon: keyof typeof Ionicons.glyphMap; text: string; side?: "left" | "right"; } @@ -36,7 +36,7 @@ export const GestureOverlay = ({ const [feedback, setFeedback] = useState({ visible: false, - icon: "", + icon: "play", text: "", }); const [fadeAnim] = useState(new Animated.Value(0)); @@ -46,7 +46,7 @@ export const GestureOverlay = ({ const showFeedback = useCallback( ( - icon: string, + icon: keyof typeof Ionicons.glyphMap, text: string, side?: "left" | "right", isDuringDrag = false, @@ -320,7 +320,7 @@ export const GestureOverlay = ({ }} > = ({ playbackPosition: playbackPosition, }).toString(); - router.replace(`player/direct-player?${queryParams}` as any); + router.replace(`player/direct-player?${queryParams}` as Href); }; const setTrackParams = ( diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index e1332e43..538c6d52 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { useLocalSearchParams, useRouter } from "expo-router"; +import { type Href, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useMemo, useRef } from "react"; import { Platform, View } from "react-native"; import { BITRATES } from "@/components/BitrateSelector"; @@ -53,7 +53,7 @@ const DropdownView = () => { bitrateValue: bitrate.toString(), playbackPosition: playbackPositionRef.current, }).toString(); - router.replace(`player/direct-player?${queryParams}` as any); + router.replace(`player/direct-player?${queryParams}` as Href); }, [audioIndex, subtitleIndex, router], ); diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index ee0e8625..9e4c0c1c 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -362,10 +362,11 @@ export const useSettings = () => { value !== undefined && _settings?.[settingsKey] !== value ) { - (unlockedPluginDefaults as any)[settingsKey] = value; + (unlockedPluginDefaults as Record)[settingsKey] = + value; } - (acc as any)[settingsKey] = locked + (acc as Record)[settingsKey] = locked ? value : (_settings?.[settingsKey] ?? value); }