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.
This commit is contained in:
Uruk
2025-11-14 23:48:59 +01:00
committed by Gauvain
parent c05cef295e
commit 75d6948a81
16 changed files with 81 additions and 57 deletions

View File

@@ -1,15 +1,16 @@
import { MMKV } from "react-native-mmkv";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
declare module "react-native-mmkv" { declare module "react-native-mmkv" {
interface MMKV { interface MMKV {
get<T>(key: string): T | undefined; get<T>(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 // Add the augmentation methods directly to the MMKV instance
// This follows the recommended pattern while adding the helper methods your app uses // We need to bind these methods to preserve the 'this' context
(storage as any).get = function <T>(key: string): T | undefined { storage.get = function <T>(this: MMKV, key: string): T | undefined {
try { try {
const serializedItem = this.getString(key); const serializedItem = this.getString(key);
if (!serializedItem) return undefined; 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 { try {
if (value === undefined) { if (value === undefined) {
this.remove(key); this.remove(key);

View File

@@ -108,10 +108,10 @@ export const BitrateSheet: React.FC<Props> = ({
values={selected ? [selected] : []} values={selected ? [selected] : []}
multiple={false} multiple={false}
searchFilter={(item, query) => { searchFilter={(item, query) => {
const label = (item as any).key || ""; const label = item.key || "";
return label.toLowerCase().includes(query.toLowerCase()); return label.toLowerCase().includes(query.toLowerCase());
}} }}
renderItemLabel={(item) => <Text>{(item as any).key || ""}</Text>} renderItemLabel={(item) => <Text>{item.key || ""}</Text>}
set={(vals) => { set={(vals) => {
const chosen = vals[0] as Bitrate | undefined; const chosen = vals[0] as Bitrate | undefined;
if (chosen) onChange(chosen); if (chosen) onChange(chosen);

View File

@@ -4,7 +4,19 @@ import type { PropsWithChildren } from "react";
import { Platform, TouchableOpacity, type ViewProps } from "react-native"; import { Platform, TouchableOpacity, type ViewProps } from "react-native";
import { useHaptic } from "@/hooks/useHaptic"; 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; onPress?: () => void;
icon?: keyof typeof Ionicons.glyphMap; icon?: keyof typeof Ionicons.glyphMap;
background?: boolean; background?: boolean;
@@ -41,7 +53,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -60,7 +72,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -78,7 +90,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -98,7 +110,7 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
className={`rounded-full ${buttonSize} flex items-center justify-center ${ className={`rounded-full ${buttonSize} flex items-center justify-center ${
fillColor ? fillColorClass : "bg-transparent" fillColor ? fillColorClass : "bg-transparent"
}`} }`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons
@@ -112,11 +124,11 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
); );
return ( return (
<TouchableOpacity onPress={handlePress} {...(viewProps as any)}> <TouchableOpacity onPress={handlePress} {...viewProps}>
<BlurView <BlurView
intensity={90} intensity={90}
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`} className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`}
{...(viewProps as any)} {...viewProps}
> >
{icon ? ( {icon ? (
<Ionicons <Ionicons

View File

@@ -81,14 +81,12 @@ export const TrackSheet: React.FC<Props> = ({
} }
multiple={false} multiple={false}
searchFilter={(item, query) => { searchFilter={(item, query) => {
const label = (item as any).DisplayTitle || ""; const label = item.DisplayTitle || "";
return label.toLowerCase().includes(query.toLowerCase()); return label.toLowerCase().includes(query.toLowerCase());
}} }}
renderItemLabel={(item) => ( renderItemLabel={(item) => <Text>{item.DisplayTitle || ""}</Text>}
<Text>{(item as any).DisplayTitle || ""}</Text>
)}
set={(vals) => { set={(vals) => {
const chosen = vals[0] as any; const chosen = vals[0];
if (chosen && chosen.Index !== null && chosen.Index !== undefined) { if (chosen && chosen.Index !== null && chosen.Index !== undefined) {
onChange(chosen.Index); onChange(chosen.Index);
} }

View File

@@ -7,7 +7,7 @@ import {
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router"; import { type Href, useRouter } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
@@ -340,7 +340,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
const navigateToItem = useCallback( const navigateToItem = useCallback(
(item: BaseItemDto) => { (item: BaseItemDto) => {
const navigation = getItemNavigation(item, "(home)"); const navigation = getItemNavigation(item, "(home)");
router.push(navigation as any); router.push(navigation as Href);
}, },
[router], [router],
); );

View File

@@ -1,6 +1,6 @@
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; 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 { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import { useFavorite } from "@/hooks/useFavorite"; import { useFavorite } from "@/hooks/useFavorite";
@@ -146,12 +146,12 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
if (isOffline) { if (isOffline) {
// For offline mode, we still need to use query params // For offline mode, we still need to use query params
const url = `${itemRouter(item, from)}&offline=true`; const url = `${itemRouter(item, from)}&offline=true`;
router.push(url as any); router.push(url as Href);
return; return;
} }
const navigation = getItemNavigation(item, from); const navigation = getItemNavigation(item, from);
router.push(navigation as any); router.push(navigation as Href);
}} }}
{...props} {...props}
> >

View File

@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useRouter, useSegments } from "expo-router"; import { type Href, useRouter, useSegments } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { Dimensions, View, type ViewProps } from "react-native"; import { Dimensions, View, type ViewProps } from "react-native";
@@ -156,7 +156,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
if (!from) return; if (!from) return;
lightHapticFeedback(); lightHapticFeedback();
const navigation = getItemNavigation(item, from); const navigation = getItemNavigation(item, from);
router.push(navigation as any); router.push(navigation as Href);
}, [item, from]); }, [item, from]);
const tap = Gesture.Tap() const tap = Gesture.Tap()

View File

@@ -1,4 +1,4 @@
import { router, useSegments } from "expo-router"; import { type Href, router, useSegments } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
@@ -21,10 +21,10 @@ const CompanySlide: React.FC<
const navigate = useCallback( const navigate = useCallback(
({ id, image, name }: Network | Studio) => ({ id, image, name }: Network | Studio) =>
router.push({ 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 }, params: { id, image, name, type: slide.type },
}), } as Href),
[slide], [slide, from],
); );
return ( return (

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; 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 type React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
@@ -18,10 +18,10 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const navigate = useCallback( const navigate = useCallback(
(genre: GenreSliderItem) => (genre: GenreSliderItem) =>
router.push({ 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 }, params: { type: slide.type, name: genre.name },
}), } as Href),
[slide], [slide, from],
); );
const { data } = useQuery({ const { data } = useQuery({

View File

@@ -31,15 +31,16 @@ export const ListGroup: React.FC<PropsWithChildren<Props>> = ({
className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900' className='flex flex-col rounded-xl overflow-hidden pl-0 bg-neutral-900'
> >
{Children.map(childrenArray, (child, index) => { {Children.map(childrenArray, (child, index) => {
if (isValidElement<{ style?: ViewStyle }>(child)) { if (isValidElement(child)) {
return cloneElement(child as any, { const style = StyleSheet.compose(
style: StyleSheet.compose( (child.props as { style?: ViewStyle }).style,
child.props.style, index < childrenArray.length - 1
index < childrenArray.length - 1 ? styles.borderBottom
? styles.borderBottom : undefined,
: undefined, );
), return cloneElement(child, { style } as Partial<
}); typeof child.props
>);
} }
return child; return child;
})} })}

View File

@@ -3,7 +3,18 @@ import type { PropsWithChildren, ReactNode } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "../common/Text"; 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; title?: string | null | undefined;
subtitle?: string | null | undefined; subtitle?: string | null | undefined;
value?: string | null | undefined; value?: string | null | undefined;
@@ -37,7 +48,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${ className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${
disabled ? "opacity-50" : "" disabled ? "opacity-50" : ""
}`} }`}
{...(viewProps as any)} {...viewProps}
> >
<ListItemContent <ListItemContent
title={title} title={title}

View File

@@ -3,7 +3,7 @@ import type {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { useLocalSearchParams, useRouter } from "expo-router"; import { type Href, useLocalSearchParams, useRouter } from "expo-router";
import { import {
type Dispatch, type Dispatch,
type FC, type FC,
@@ -379,7 +379,7 @@ export const Controls: FC<Props> = ({
console.log("queryParams", queryParams); 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], [settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
); );

View File

@@ -18,7 +18,7 @@ interface Props {
interface FeedbackState { interface FeedbackState {
visible: boolean; visible: boolean;
icon: string; icon: keyof typeof Ionicons.glyphMap;
text: string; text: string;
side?: "left" | "right"; side?: "left" | "right";
} }
@@ -36,7 +36,7 @@ export const GestureOverlay = ({
const [feedback, setFeedback] = useState<FeedbackState>({ const [feedback, setFeedback] = useState<FeedbackState>({
visible: false, visible: false,
icon: "", icon: "play",
text: "", text: "",
}); });
const [fadeAnim] = useState(new Animated.Value(0)); const [fadeAnim] = useState(new Animated.Value(0));
@@ -46,7 +46,7 @@ export const GestureOverlay = ({
const showFeedback = useCallback( const showFeedback = useCallback(
( (
icon: string, icon: keyof typeof Ionicons.glyphMap,
text: string, text: string,
side?: "left" | "right", side?: "left" | "right",
isDuringDrag = false, isDuringDrag = false,
@@ -320,7 +320,7 @@ export const GestureOverlay = ({
}} }}
> >
<Ionicons <Ionicons
name={feedback.icon as any} name={feedback.icon}
size={24} size={24}
color='white' color='white'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}

View File

@@ -1,5 +1,5 @@
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client"; import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
import { router, useLocalSearchParams } from "expo-router"; import { type Href, router, useLocalSearchParams } from "expo-router";
import type React from "react"; import type React from "react";
import { import {
createContext, createContext,
@@ -95,7 +95,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
playbackPosition: playbackPosition, playbackPosition: playbackPosition,
}).toString(); }).toString();
router.replace(`player/direct-player?${queryParams}` as any); router.replace(`player/direct-player?${queryParams}` as Href);
}; };
const setTrackParams = ( const setTrackParams = (

View File

@@ -1,5 +1,5 @@
import { Ionicons } from "@expo/vector-icons"; 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 { useCallback, useMemo, useRef } from "react";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector"; import { BITRATES } from "@/components/BitrateSelector";
@@ -53,7 +53,7 @@ const DropdownView = () => {
bitrateValue: bitrate.toString(), bitrateValue: bitrate.toString(),
playbackPosition: playbackPositionRef.current, playbackPosition: playbackPositionRef.current,
}).toString(); }).toString();
router.replace(`player/direct-player?${queryParams}` as any); router.replace(`player/direct-player?${queryParams}` as Href);
}, },
[audioIndex, subtitleIndex, router], [audioIndex, subtitleIndex, router],
); );

View File

@@ -362,10 +362,11 @@ export const useSettings = () => {
value !== undefined && value !== undefined &&
_settings?.[settingsKey] !== value _settings?.[settingsKey] !== value
) { ) {
(unlockedPluginDefaults as any)[settingsKey] = value; (unlockedPluginDefaults as Record<string, unknown>)[settingsKey] =
value;
} }
(acc as any)[settingsKey] = locked (acc as Record<string, unknown>)[settingsKey] = locked
? value ? value
: (_settings?.[settingsKey] ?? value); : (_settings?.[settingsKey] ?? value);
} }