Dev experience: linting for TypeScript (#2933)

* Change TS warning about unused variables to error & support _unused in catch blocks

* Use TS recommended object instead of Object

* Set Function issues to warnings in TS files for later fixing
This commit is contained in:
Amanda Bullington
2025-06-02 09:47:29 -07:00
committed by GitHub
parent bb31c1907b
commit 502393a5d1
54 changed files with 124 additions and 108 deletions

View File

@@ -70,8 +70,6 @@ module.exports = {
],
"no-alert": 0,
"no-underscore-dangle": 0,
// This gets around eslint problems when typing functions in TS
"no-unused-vars": 0,
"no-void": 0,
"prefer-destructuring": [2, { object: true, array: false }],
quotes: [2, "double"],
@@ -127,27 +125,23 @@ module.exports = {
"no-undef": "error",
"@typescript-eslint/no-unused-vars": [
"warn",
"error",
{
vars: "all",
args: "after-used",
// Overriding airbnb to allow leading underscore to indicate unused var
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true
ignoreRestSiblings: true,
caughtErrors: "all",
// needed a special case for catch blocks that use _ to define an unused error
caughtErrorsIgnorePattern: "^_"
}
],
// TODO: we should actually type these at some point ~amanda 041824
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-wrapper-object-types": 0,
"@typescript-eslint/no-require-imports": 0,
"@typescript-eslint/no-unsafe-function-type": 0,
"@typescript-eslint/no-empty-object-types": 0,
"@typescript-eslint/no-empty-object-type": 0,
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-unused-expressions": 1
"@typescript-eslint/no-require-imports": ["error", {
allow: ["\\.(png|jpg|jpeg|gif|svg)$"]
}],
"@typescript-eslint/no-unsafe-function-type": 1
},
// need this so jest doesn't show as undefined in jest.setup.js
env: {
@@ -161,5 +155,21 @@ module.exports = {
extensions: [".js", ".jsx", ".ts", ".tsx"]
}
}
}
},
overrides: [
{
files: ["*.js", "*.jsx"],
rules: {
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/no-require-imports": "off"
}
},
{
files: ["**/__mocks__/**/*", "**/*mock*", "**/*.mock.*"],
rules: {
"@typescript-eslint/no-require-imports": "off"
}
}
]
};

View File

@@ -13,6 +13,6 @@ export default async function dismissAnnouncements() {
await element( by.id( "announcements-dismiss" ) ).tap();
} catch ( error ) {
// if timeout occurs, the element isn't visible, so continue with test
console.log( "No announcement present, continuing with test" );
console.log( "No announcement present, continuing with test", error );
}
}

View File

@@ -118,7 +118,7 @@ const downloadIOS = async () => {
console.log( "iOS done!" );
};
// eslint-disable-next-line no-unused-expressions
// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions
yargs
.usage( "Usage: $0 [args]" )
.option( "androidFlavor", {

View File

@@ -16,7 +16,7 @@ const PARAMS = {
const scoreImage = async (
params = {},
opts = {}
): Promise<Object> => {
): Promise<object> => {
try {
return inatjs.computervision.score_image( { ...PARAMS, ...params }, opts );
} catch ( e ) {

View File

@@ -31,7 +31,7 @@ const iNatLogstashTransport: transportFunctionType = async props => {
let userToken;
try {
userToken = await getJWT();
} catch ( getJWTError ) {
} catch ( _getJWTError ) {
// We use logging to report errors, so this is one of the few cases where
// we really do want to squelch all errors to avoid recursion
console.error( "[ERROR log.ts] failed to retrieve user JWT while logging" );

View File

@@ -14,7 +14,7 @@ const PARAMS = {
fields: MESSAGE_FIELDS
};
const searchMessages = async ( params: Object = {}, opts: Object = {} ): Promise<Object> => {
const searchMessages = async ( params: object = {}, opts: object = {} ): Promise<object> => {
try {
const { results } = await inatjs.messages.search( { ...PARAMS, ...params }, opts );
return results;

View File

@@ -42,7 +42,7 @@ const search = async (
try {
response = await inatjs.search( { ...PARAMS, ...params }, opts );
} catch ( searchError ) {
handleError( searchError as Object );
handleError( searchError as object );
// handleError should throw, so in theory this should never happen and
// this is just to placate typescript
return null;

4
src/api/types.d.ts vendored
View File

@@ -6,7 +6,7 @@ export interface ApiOpts {
export interface ApiParams {
per_page?: number;
page?: number;
fields?: "all" | Object;
fields?: "all" | object;
ttl?: number;
}
@@ -26,7 +26,7 @@ export interface ApiResponse {
total_results: number;
page: number;
per_page: number;
results: Object[];
results: object[];
}
export interface ApiObservationsUpdatesParams extends ApiParams {

View File

@@ -13,15 +13,15 @@ const isTablet = DeviceInfo.isTablet( );
interface Props {
cameraType: string,
device: CameraDevice,
camera: Object,
camera: object,
flipCamera: ( ) => void,
handleCheckmarkPress: ( ) => void,
toggleFlash: Function,
takingPhoto: boolean,
takePhotoAndStoreUri: Function,
newPhotoUris: Array<Object>,
newPhotoUris: Array<object>,
setNewPhotoUris: Function,
takePhotoOptions: Object,
takePhotoOptions: object,
userLocation: UserLocation | null,
hasLocationPermissions: boolean,
requestLocationPermissions: () => void,

View File

@@ -4,7 +4,7 @@ import React from "react";
import { Animated } from "react-native";
interface Props {
animatedStyle: Object
animatedStyle: object
}
const FocusSquare = ( { animatedStyle }: Props ) => {

View File

@@ -24,7 +24,7 @@ const logger = log.extend( "savePhotosToPhotoLibrary" );
// $FlowIgnore
export async function savePhotosToPhotoLibrary(
uris: [string],
location: Object
location: object
) {
const readWritePermissionResult = permissionResultFromMultiple(
await checkMultiple( READ_WRITE_MEDIA_PERMISSIONS )

View File

@@ -24,7 +24,7 @@ const MAX_ZOOM_FACTOR = 20;
// Used for calculating the final zoom by pinch gesture
const SCALE_FULL_ZOOM = 3;
const useZoom = ( device: CameraDevice ): Object => {
const useZoom = ( device: CameraDevice ): object => {
const initialZoomTextValue = "1";
const zoomButtonOptions = useMemo( () => {
const options = [initialZoomTextValue];

View File

@@ -15,7 +15,7 @@ type Props = {
options: {
title?: string | undefined;
headerTitle?: HeaderTitle;
headerStyle?: Object;
headerStyle?: object;
headerShadowVisible?: boolean;
};
};

View File

@@ -159,7 +159,7 @@ const signOut = async (
try {
options.realm.deleteAll( );
options.realm.commitTransaction( );
} catch ( realmError ) {
} catch ( _realmError ) {
options.realm.cancelTransaction( );
// If we failed to wipe all the data in realm, delete the realm file.
// Note that deleting the realm file *all* the time seems to cause
@@ -198,7 +198,7 @@ const signOut = async (
* Encodes a JWT. Lifted from react-native-jwt-io
* https://github.com/maxweb4u/react-native-jwt-io/blob/7f926da46ff536dbb531dd8ae7177ab4ff28c43f/src/jwt.js#L21
*/
const encodeJWT = ( payload: Object, key: string, algorithm?: string ) => {
const encodeJWT = ( payload: object, key: string, algorithm?: string ) => {
algorithm = typeof algorithm !== "undefined"
? algorithm
: "HS256";

View File

@@ -27,7 +27,7 @@ export interface Props {
source: {
uri: string
};
style?: Object;
style?: object;
speciesCount: SpeciesCount;
}

View File

@@ -44,7 +44,7 @@ const useSyncObservations = (
const realm = useRealm( );
const handleRemoteDeletion = useAuthenticatedMutation(
( params: Object, optsWithAuth: Object ) => deleteRemoteObservation( params, optsWithAuth ),
( params: object, optsWithAuth: object ) => deleteRemoteObservation( params, optsWithAuth ),
{
onSuccess: ( ) => {
Observation
@@ -75,7 +75,7 @@ const useSyncObservations = (
} else {
try {
await handleRemoteDeletion.mutateAsync( { uuid } );
} catch ( error ) {
} catch ( _error ) {
// In case of failure, clear the pending deletion flag after some time
// to allow retrying later
setTimeout( ( ) => {

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "sharedHooks";
interface Props {
textClassName?: string;
currentUser: { id: number };
observation: Object;
observation: object;
}
const ObscurationExplanation = ( { textClassName, currentUser, observation }: Props ) => {

View File

@@ -15,10 +15,10 @@ const sectionClass = "mx-[15px] mb-[20px]";
interface Props {
observation: {
project_observations: Array<{
project: Object;
project: object;
}>;
non_traditional_projects: Array<{
project: Object;
project: object;
}>;
}
}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "sharedHooks";
interface Props {
textClassName?: string;
currentUser: { id: number };
observation: Object;
observation: object;
}
const ObscurationExplanation = ( { textClassName, currentUser, observation }: Props ) => {

View File

@@ -7,10 +7,10 @@ import React, { useMemo } from "react";
interface Props {
observation: {
project_observations: Array<{
project: Object;
project: object;
}>;
non_traditional_projects: Array<{
project: Object;
project: object;
}>;
}
}

View File

@@ -18,7 +18,7 @@ interface Props {
observationId: number,
uuid: string,
refetchSubscriptions: Function,
subscriptions: Object
subscriptions: object
}
const ObsDetailsDefaultModeHeaderRight = ( {

View File

@@ -22,7 +22,7 @@ const { useRealm } = RealmContext;
type Props = {
passesEvidenceTest: boolean,
observations: Array<Object>,
observations: Array<object>,
currentObservation: RealmObservation,
currentObservationIndex: number,
setCurrentObservationIndex: Function,

View File

@@ -7,7 +7,7 @@ import ObsGridItem from "./ObsGridItem";
import ObsListItem from "./ObsListItem";
type Props = {
currentUser: Object,
currentUser: object,
queued: boolean,
explore: boolean,
hideMetadata?: boolean,
@@ -17,7 +17,7 @@ type Props = {
hideRGLabel?: boolean,
onUploadButtonPress: ( ) => void,
onItemPress: ( ) => void,
gridItemStyle: Object,
gridItemStyle: object,
layout: "list" | "grid",
observation: RealmObservation,
uploadProgress: number,

View File

@@ -6,7 +6,7 @@ import React, { useState } from "react";
import { useCurrentUser, useTranslation } from "sharedHooks";
interface Props {
rule: Object
rule: object
}
const ProjectRuleItem = ( { rule }: Props ) => {

View File

@@ -12,7 +12,7 @@ import {
import ProjectListItem from "./ProjectListItem";
interface Props {
projects: Array<Object>
projects: Array<object>
ListEmptyComponent?: React.JSX.Element
ListFooterComponent?: React.JSX.Element
onEndReached?: ( ) => void

View File

@@ -35,7 +35,7 @@ interface Props {
isFetchingNextPage: boolean;
isLoading: boolean;
memberId?: number;
projects: Object[],
projects: object[],
requestPermissions: () => void;
searchInput: string;
setSearchInput: ( _text: string ) => void;

View File

@@ -4,7 +4,7 @@ import {
useAuthenticatedInfiniteQuery
} from "sharedHooks";
const useInfiniteProjectsScroll = ( { params: newInputParams, enabled }: Object ): Object => {
const useInfiniteProjectsScroll = ( { params: newInputParams, enabled }: object ): object => {
const baseParams = {
...newInputParams,
per_page: 50,

View File

@@ -11,7 +11,7 @@ import {
I18nManager
} from "react-native";
interface Props extends FlatListProps<Object> {
interface Props extends FlatListProps<object> {
onSlideScroll: ( index: number ) => void;
}

View File

@@ -20,7 +20,14 @@ type Props = {
login: string
},
isConnected: boolean,
TextComponent: any,
TextComponent: ( props: {
className?: string,
numberOfLines?: number,
ellipsizeMode?: string,
selectable?: boolean,
maxFontSizeMultiplier?: number,
children?: React$Node, // eslint-disable-line no-undef
} ) => React$Node, // eslint-disable-line no-undef
testID: string,
useBigIcon?: boolean
};

View File

@@ -76,7 +76,7 @@ interface Props {
style?: ViewStyle;
switchMapTypeButtonClassName?: string;
testID?: string;
tileMapParams?: Object | null;
tileMapParams?: object | null;
withObsTiles?: boolean;
withPressableObsTiles?: boolean;
zoomEnabled?: boolean;

View File

@@ -12,7 +12,7 @@ import { useCurrentUser, useFontScale, useTranslation } from "sharedHooks";
export interface Props {
headerText?: string;
showSpeciesSeenCheckmark?: boolean;
style?: Object;
style?: object;
taxon: ApiTaxon;
upperRight?: React.ReactNode;
}

View File

@@ -28,14 +28,14 @@ interface TaxonResultProps {
fetchRemote?: boolean;
first?: boolean;
fromLocal?: boolean;
handleCheckmarkPress: ( taxon: Object ) => void;
handleCheckmarkPress: ( taxon: object ) => void;
handleRemovePress?: () => void;
handleTaxonOrEditPress?: ( _event?: GestureResponderEvent ) => void;
hideInfoButton?: boolean;
hideNavButtons?: boolean;
checkmarkFocused?: boolean;
lastScreen?: string | null;
onPressInfo?: ( taxon: Object ) => void;
onPressInfo?: ( taxon: object ) => void;
showCheckmark?: boolean;
showEditButton?: boolean;
showRemoveButton?: boolean;

View File

@@ -93,7 +93,7 @@ const LINKIFY_OPTIONS: Opts = {
interface Props extends React.PropsWithChildren {
text: string,
htmlStyle?: Object,
htmlStyle?: object,
}
const UserText = ( {

View File

@@ -4,7 +4,7 @@ import useStore from "stores/useStore";
const useNavigateWithTaxonSelected = (
// Navigation happens when a taxon was selected
selectedTaxon: Object | null | undefined,
selectedTaxon: object | null | undefined,
// After navigation we need to unselect the taxon so we don't have
// mysterious background nonsense happening after this screen loses focus
unselectTaxon: Function,

View File

@@ -18,7 +18,7 @@ interface Props {
ListFooterComponent?: React.JSX.Element
onEndReached?: ( ) => void
refreshing?: boolean
users: Array<Object>
users: Array<object>
onPress?: ( ) => void
accessibilityLabel?: string
keyboardShouldPersistTaps?: string

View File

@@ -9,7 +9,7 @@ import User from "realmModels/User.ts";
import { useTranslation } from "sharedHooks";
interface Props {
item: Object
item: object
countText: string
onPress?: Function
accessibilityLabel?: string

View File

@@ -44,7 +44,7 @@ const writeLoadTranslations = async ( ) => {
out.write( "\n];\n" );
};
// eslint-disable-next-line no-unused-expressions
// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions
yargs
.usage( "Usage: $0 <cmd> [args]" )
.option( "verbose", {

View File

@@ -211,7 +211,7 @@ async function renameDirectories( options = {} ) {
if ( options.verbose ) {
console.log( "Removed existing directory", newDirectoryPath );
}
} catch ( e ) {
} catch ( _e ) {
// Directory did not exist
}
if ( options.verbose ) {
@@ -242,7 +242,7 @@ async function renameDirectories( options = {} ) {
if ( options.verbose ) {
console.log( "Removed existing directory", newDirectoryPath );
}
} catch ( e ) {
} catch ( _e ) {
// Directory did not exist
}
if ( options.verbose ) {

View File

@@ -44,9 +44,9 @@ const drawerScrollViewStyle = {
} as const;
interface Props {
state: Object;
navigation: Object;
descriptors: Object;
state: object;
navigation: object;
descriptors: object;
}
const feedbackLogger = log.extend( "feedback" );

View File

@@ -203,9 +203,9 @@ type State = {
place_guess: string,
placeMode: PLACE_MODE,
place_id: number | null | undefined,
// TODO: technically this is not any Object but a "Project"
// TODO: technically this is not any object but a "Project"
// and should be typed as such (e.g., in realm model)
project: Object | undefined | null,
project: object | undefined | null,
project_id: number | undefined | null,
radius?: number,
researchGrade: boolean,
@@ -214,26 +214,26 @@ type State = {
sortBy: SORT_BY,
swlat?: number,
swlng?: number,
// TODO: technically this is not any Object but a "Taxon"
// TODO: technically this is not any object but a "Taxon"
// and should be typed as such (e.g., in realm model)
taxon: Object | undefined | null,
taxon: object | undefined | null,
taxon_id: number | undefined | null,
// TODO: technically this is not any Object but a "User"
// TODO: technically this is not any object but a "User"
// and should be typed as such (e.g., in realm model)
user: Object | undefined | null,
user: object | undefined | null,
user_id: number | undefined | null,
excludeUser: Object | undefined | null,
excludeUser: object | undefined | null,
verifiable: boolean,
wildStatus: WILD_STATUS
}
type Action = {type: EXPLORE_ACTION.RESET}
| {type: EXPLORE_ACTION.DISCARD, snapshot: State}
| {type: EXPLORE_ACTION.SET_USER, user: Object | null, userId: number | null, storedState: State}
| {type: EXPLORE_ACTION.SET_USER, user: object | null, userId: number | null, storedState: State}
| {
type: EXPLORE_ACTION.EXCLUDE_USER,
user: null,
userId: null,
excludeUser: Object,
excludeUser: object,
storedState: State
}
| {
@@ -261,7 +261,7 @@ type Action = {type: EXPLORE_ACTION.RESET}
| {type: EXPLORE_ACTION.SET_PLACE_MODE_PLACE}
| {
type: EXPLORE_ACTION.SET_PROJECT,
project: Object | null,
project: object | null,
projectId: number | null,
storedState: State
}

View File

@@ -10,7 +10,7 @@ export async function openInbox() {
let isSupported;
try {
isSupported = await Linking.canOpenURL( "message:0" );
} catch ( canOpenURLError ) {
} catch ( _canOpenURLError ) {
openInboxError();
return;
}

View File

@@ -47,10 +47,10 @@ export const predictImage = ( uri: string, location: Location ) => {
let url;
try {
url = new URL( uri );
} catch ( urlError ) {
} catch ( _urlError ) {
try {
url = new URL( `file://${uri}` );
} catch ( urlError2 ) {
} catch ( _urlError2 ) {
// will handle when url is blank
}
}

View File

@@ -161,7 +161,7 @@ interface User {
export function accessibleTaxonName(
taxon: Taxon,
user: User | null,
t: ( key: string, options: {} ) => string
t: ( key: string, options: object ) => string
) {
const { commonName, scientificName } = generateTaxonPieces( taxon );
if ( typeof ( user?.prefers_scientific_name_first ) === "boolean" ) {

View File

@@ -10,8 +10,8 @@ import { useCurrentUser } from "sharedHooks";
const useAuthenticatedInfiniteQuery = (
queryKey: Array<string>,
queryFunction: Function,
queryOptions: Object = {}
): Object => {
queryOptions: object = {}
): object => {
const route = useRoute( );
const currentUser = useCurrentUser( );

View File

@@ -4,12 +4,12 @@ import { useAuthenticatedInfiniteQuery } from "sharedHooks";
const useInfiniteUserScroll = (
queryKey: string,
apiCall: Function,
ids: Array<Object>,
newInputParams: Object,
ids: Array<object>,
newInputParams: object,
options: {
enabled: boolean
}
): Object => {
): object => {
const baseParams = {
...newInputParams,
// TODO: can change this once API pagination is working

View File

@@ -6,8 +6,8 @@ import { useSafeRoute } from "sharedHooks";
const useNonAuthenticatedQuery = (
queryKey: Array<string>,
queryFunction: Function,
queryOptions: Object = {}
): Object => {
queryOptions: object = {}
): object => {
const route = useSafeRoute( );
return useQuery( {

View File

@@ -18,7 +18,7 @@ const useSafeRoute = () => {
routeParams: route.params
};
}
} catch ( e ) {
} catch ( _e ) {
// console.log( "Route not available from useSafeRoute" );
}

View File

@@ -21,16 +21,16 @@ const { useRealm } = RealmContext;
type OnlineSuggestionsResponse = {
dataUpdatedAt: Date,
onlineSuggestions: Object,
onlineSuggestions: object,
loadingOnlineSuggestions: boolean,
timedOut: boolean,
error: Object,
error: object,
resetTimeout: Function
isRefetching: boolean
}
const useOnlineSuggestions = (
options: Object
options: object
): OnlineSuggestionsResponse => {
const realm = useRealm( );
const {

View File

@@ -6,8 +6,8 @@ const DEFAULT_STATE = {
};
interface RootExploreSlice {
rootStoredParams: Object,
setRootStoredParams: ( _params: Object ) => void,
rootStoredParams: object,
setRootStoredParams: ( _params: object ) => void,
rootExploreView: string,
setRootExploreView: ( _view: string ) => void
}

View File

@@ -23,7 +23,7 @@ interface TotalUploadProgress {
interface UploadObservationsSlice {
abortController: AbortController | null,
currentUpload: RealmObservation | null,
errorsByUuid: Object,
errorsByUuid: object,
multiError: string | null,
initialNumObservationsInQueue: number,
numUnuploadedObservations: number,

View File

@@ -12,7 +12,6 @@ declare module "*.svg" {
declare global {
namespace ReactNavigation {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RootParamList extends RootStackParamList {}
type RootParamList = RootStackParamList
}
}

View File

@@ -2,11 +2,11 @@ import ObservationPhoto from "realmModels/ObservationPhoto";
import ObservationSound from "realmModels/ObservationSound";
function prepareMediaForUpload(
media: Object,
media: object,
type: string,
action: "upload" | "attach" | "update",
observationId?: number | null
): Object {
): object {
if ( type === "Photo" || type === "ObservationPhoto" ) {
if ( action === "upload" ) {
return ObservationPhoto.mapPhotoForUpload( observationId, media );

View File

@@ -1,6 +1,6 @@
import Observation from "realmModels/Observation";
function prepareObservationForUpload( obs: Object ): Object {
function prepareObservationForUpload( obs: object ): object {
const obsToUpload = Observation.mapObservationForUpload( obs );
// Remove all null values, b/c the API doesn't seem to like them for some

View File

@@ -1,14 +1,14 @@
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
function findRecordInRealm(
realm: Object,
realm: object,
observationUUID: string,
recordUUID: string | null,
type: string,
options?: {
record: Object
record: object
}
): Object | null {
): object | null {
if ( !realm || realm.isClosed ) return null;
// Photos do not have UUIDs, so we pass the Photo itself as an option
@@ -31,8 +31,8 @@ function findRecordInRealm(
}
function updateRecordWithServerId(
realm: Object,
record: Object,
realm: object,
record: object,
serverId: number,
type: string
): void {
@@ -47,13 +47,13 @@ function updateRecordWithServerId(
function handleRecordUpdateError(
error: Error,
realm: Object,
realm: object,
observationUUID: string,
recordUUID: string | null,
type: string,
serverId: number,
options?: {
record: Object
record: object
}
): void {
// Try it one more time in case it was invalidated but it's still in the
@@ -86,9 +86,9 @@ const markRecordUploaded = (
response: {
results: Array<{id: number}>
},
realm: Object,
realm: object,
options?: {
record: Object
record: object
}
) => {
if ( !realm || realm.isClosed ) return;