Другое

Как исправить ошибки TypeScript/React: Unexpected any, {} и display name

Полное руководство по исправлению ошибок ESLint в React/TypeScript: Unexpected any, empty object type {} и missing display name. Конкретные решения для улучшения качества кода.

Как исправить ошибки TypeScript/React: ‘Unexpected any’, ‘empty object type {}’ и ‘Component definition is missing display name’?

У меня возникли следующие ошибки ESLint в компоненте React с TypeScript:

✖ 6 errors (6 errors, 0 warnings):

  • Unexpected any. Specify a different type (3 occurrences)
  • The {} (“empty object”) type allows any non-nullish value (2 occurrences)
  • Component definition is missing display name

Вот код компонента, вызывающий эти ошибки:

typescript
import { type UseTRPCQueryResult, type UseTRPCQuerySuccessResult } from '@trpc/react-query/shared'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { ErrorPageComponent } from '../components/ErrorPageComponent'
import { NotFoundPage } from '../pages/other/NotFoundPage'
import { useAppContext, type AppContext } from './ctx'
import { getAllIdeasRoute } from './routes'

class CheckExistsError extends Error {}
const checkExistsFn = <T,>(value: T, message?: string): NonNullable<T> => {
  if (!value) {
    throw new CheckExistsError(message)
  }
  return value
}

class CheckAccessError extends Error {}
const checkAccessFn = <T,>(value: T, message?: string): void => {
  if (!value) {
    throw new CheckAccessError(message)
  }
}

type Props = Record<string, any>
type QueryResult = UseTRPCQueryResult<any, any>
type QuerySuccessResult<TQueryResult extends QueryResult> = UseTRPCQuerySuccessResult<
  NonNullable<TQueryResult['data']>,
  null
>
type HelperProps<TQueryResult extends QueryResult | undefined> = {
  ctx: AppContext
  queryResult: TQueryResult extends QueryResult ? QuerySuccessResult<TQueryResult> : undefined
}
type SetPropsProps<TQueryResult extends QueryResult | undefined> = HelperProps<TQueryResult> & {
  checkExists: typeof checkExistsFn
  checkAccess: typeof checkAccessFn
}
type PageWrapperProps<TProps extends Props, TQueryResult extends QueryResult | undefined> = {
  redirectAuthorized?: boolean
  authorizedOnly?: boolean
  authorizedOnlyTitle?: string
  authorizedOnlyMessage?: string
  checkAccess?: (helperProps: HelperProps<TQueryResult>) => boolean
  checkAccessTitle?: string
  checkAccessMessage?: string
  checkExists?: (helperProps: HelperProps<TQueryResult>) => boolean
  checkExistsTitle?: string
  checkExistsMessage?: string
  useQuery?: () => TQueryResult
  setProps?: (setPropsProps: SetPropsProps<TQueryResult>) => TProps
  Page: React.FC<TProps>
}

const PageWrapper = <TProps extends Props = {}, TQueryResult extends QueryResult | undefined = undefined>({
  authorizedOnly,
  authorizedOnlyTitle = 'Please, Authorize',
  authorizedOnlyMessage = 'This page is available only for authorized users',
  redirectAuthorized,
  checkAccess,
  checkAccessTitle = 'Access Denied',
  checkAccessMessage = 'You have no access to this page',
  checkExists,
  checkExistsTitle,
  checkExistsMessage,
  useQuery,
  setProps,
  Page,
}: PageWrapperProps<TProps, TQueryResult>) => {
  const navigate = useNavigate()
  const ctx = useAppContext()
  const queryResult = useQuery?.()

  const redirectNeeded = redirectAuthorized && ctx.me

  useEffect(() => {
    if (redirectNeeded) {
      navigate(getAllIdeasRoute(), { replace: true })
    }
  }, [redirectNeeded, navigate])

  if (queryResult?.isLoading || queryResult?.isFetching || redirectNeeded) {
    return <p>Loading...</p>
  }

  if (queryResult?.isError) {
    return <ErrorPageComponent message={queryResult.error.message} />
  }

  if (authorizedOnly && !ctx.me) {
    return <ErrorPageComponent title={authorizedOnlyTitle} message={authorizedOnlyMessage} />
  }

  const helperProps = { ctx, queryResult: queryResult as never }

  if (checkAccess) {
    const accessDenied = !checkAccess(helperProps)
    if (accessDenied) {
      return <ErrorPageComponent title={checkAccessTitle} message={checkAccessMessage} />
    }
  }

  if (checkExists) {
    const notExists = !checkExists(helperProps)
    if (notExists) {
      return <NotFoundPage title={checkExistsTitle} message={checkExistsMessage} />
    }
  }

  try {
    const props = setProps?.({ ...helperProps, checkExists: checkExistsFn, checkAccess: checkAccessFn }) as TProps
    return <Page {...props} />
  } catch (error) {
    if (error instanceof CheckExistsError) {
      return <NotFoundPage title={checkExistsTitle} message={error.message || checkExistsMessage} />
    }
    if (error instanceof CheckAccessError) {
      return <ErrorPageComponent title={checkAccessTitle} message={error.message || checkAccessMessage} />
    }
    throw error
  }
}

export const withPageWrapper = <TProps extends Props = {}, TQueryResult extends QueryResult | undefined = undefined>(
  pageWrapperProps: Omit<PageWrapperProps<TProps, TQueryResult>, 'Page'>
) => {
  return (Page: PageWrapperProps<TProps, TQueryResult>['Page']) => {
    return () => <PageWrapper {...pageWrapperProps} Page={Page} />
  }
}

Как правильно исправить эти ошибки TypeScript/React, сохранив функциональность компонента? Нужны решения для @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type и react/display-name.

Чтобы исправить эти ошибки TypeScript/React, вам нужно заменить тип any на более конкретные типы, использовать вместо {} тип Record<string, never> или object, а добавить displayName для компонента. Вот подробное решение для каждого типа ошибок.

Contents


Исправление ошибки “Unexpected any”

Ошибка Unexpected any. Specify a different type возникает из-за использования типа any, который противоречит принципам строгой типизации TypeScript.

Почему это важно:

Как указано в документации typescript-eslint, тип any отключает проверку типов и может приводить к ошибкам времени выполнения. Правило no-explicit-any рекомендуется включать в проекты для поддержания качества кода.

Способы исправления:

  1. Используйте unknown вместо any - это самый безопасный подход, требующий явного приведения типов
  2. Определяйте конкретные типы - создайте интерфейсы или типы для данных, с которыми работаете
  3. Используйте дженерики - как в вашем коде, для обобщенных функций и компонентов

Пример исправления:

typescript
// Было:
type Props = Record<string, any>
type QueryResult = UseTRPCQueryResult<any, any>

// Стало:
interface BaseProps {
  [key: string]: unknown
}

interface QueryResultData<TData> {
  data: TData | null
  error: Error | null
  // ... другие поля из UseTRPCQueryResult
}

type QueryResult<TData> = UseTRPCQueryResult<TData, Error>

Исправление ошибки “empty object type {}”

Ошибка The {} ("empty object") type allows any non-nullish value возникает потому, что в TypeScript пустой объектный литерал {} на самом деле позволяет любые значения, кроме null и undefined.

Почему это происходит:

Согласно исходному коду правила, тип {} эквивалентен Record<string, unknown> | number | string | boolean | symbol | bigint | null | undefined, что делает его слишком широким.

Способы исправления:

  1. Используйте Record<string, never> - явно указывает, что объект не должен иметь свойств
  2. Используйте object - базовый тип для всех не-null значений, не являющихся примитивами
  3. Определите конкретный интерфейс - когда вы точно знаете, какие свойства должны быть

Пример исправления:

typescript
// Было:
type HelperProps<TQueryResult extends QueryResult | undefined> = {
  ctx: AppContext
  queryResult: TQueryResult extends QueryResult ? QuerySuccessResult<TQueryResult> : undefined
}

// Стало:
type HelperProps<TQueryResult extends QueryResult | undefined> = {
  ctx: AppContext
  queryResult: TQueryResult extends QueryResult ? QuerySuccessResult<TQueryResult> : undefined
} & Record<string, never> // Указываем, что дополнительных свойств быть не должно

Исправление ошибки “missing display name”

Ошибка Component definition is missing display name возникает, когда у функционального компонента нет свойства displayName, которое помогает в отладке и разработке.

Почему это важно:

Как объясняется в документации eslint-plugin-react, displayName помогает:

  • Легко идентифицировать компоненты в React DevTools
  • Улучшает читаемость стека вызовов при ошибках
  • Полезен при использовании React.memo и HOC

Способы исправления:

  1. Добавить displayName явно - присвоить строку переменной компонента
  2. Использовать React.memo с именем - для мемоизированных компонентов
  3. Использовать HOC с displayName - для компонентов оберток

Пример исправления:

typescript
// Было:
const PageWrapper = <TProps extends Props = {}, TQueryResult extends QueryResult | undefined = undefined>({
  // ... пропсы
}) => {
  // ... логика компонента
}

// Стало:
const PageWrapper = <TProps extends Props = {}, TQueryResult extends QueryResult | undefined = undefined>({
  // ... пропсы
}) => {
  // ... логика компонента
}

PageWrapper.displayName = 'PageWrapper'

// Или с React.memo:
const MemoizedPageWrapper = React.memo(PageWrapper)
MemoizedPageWrapper.displayName = 'PageWrapper'

Полное решение для вашего компонента

Вот исправленная версия вашего кода с решением всех трех ошибок:

typescript
import { type UseTRPCQueryResult, type UseTRPCQuerySuccessResult } from '@trpc/react-query/shared'
import React, { useEffect, memo } from 'react'
import { useNavigate } from 'react-router-dom'
import { ErrorPageComponent } from '../components/ErrorPageComponent'
import { NotFoundPage } from '../pages/other/NotFoundPage'
import { useAppContext, type AppContext } from './ctx'
import { getAllIdeasRoute } from './routes'

class CheckExistsError extends Error {}
const checkExistsFn = <T,>(value: T, message?: string): NonNullable<T> => {
  if (!value) {
    throw new CheckExistsError(message)
  }
  return value
}

class CheckAccessError extends Error {}
const checkAccessFn = <T,>(value: T, message?: string): void => {
  if (!value) {
    throw new CheckAccessError(message)
  }
}

// Заменяем any на более конкретные типы
interface BaseProps {
  [key: string]: unknown
}

interface QueryResultData<TData> {
  data: TData | null
  error: Error | null
  isLoading: boolean
  isFetching: boolean
  isError: boolean
}

type QueryResult<TData> = UseTRPCQueryResult<TData, Error>
type QuerySuccessResult<TQueryResult extends QueryResult<any>> = UseTRPCQuerySuccessResult<
  NonNullable<TQueryResult['data']>,
  null
>

type HelperProps<TQueryResult extends QueryResult<any> | undefined> = {
  ctx: AppContext
  queryResult: TQueryResult extends QueryResult<any> ? QuerySuccessResult<TQueryResult> : undefined
} & Record<string, never>

type SetPropsProps<TQueryResult extends QueryResult<any> | undefined> = HelperProps<TQueryResult> & {
  checkExists: typeof checkExistsFn
  checkAccess: typeof checkAccessFn
}
type PageWrapperProps<TProps extends BaseProps, TQueryResult extends QueryResult<any> | undefined> = {
  redirectAuthorized?: boolean
  authorizedOnly?: boolean
  authorizedOnlyTitle?: string
  authorizedOnlyMessage?: string
  checkAccess?: (helperProps: HelperProps<TQueryResult>) => boolean
  checkAccessTitle?: string
  checkAccessMessage?: string
  checkExists?: (helperProps: HelperProps<TQueryResult>) => boolean
  checkExistsTitle?: string
  checkExistsMessage?: string
  useQuery?: () => TQueryResult
  setProps?: (setPropsProps: SetPropsProps<TQueryResult>) => TProps
  Page: React.FC<TProps>
}

const PageWrapper = <TProps extends BaseProps = {}, TQueryResult extends QueryResult<any> | undefined = undefined>({
  authorizedOnly,
  authorizedOnlyTitle = 'Please, Authorize',
  authorizedOnlyMessage = 'This page is available only for authorized users',
  redirectAuthorized,
  checkAccess,
  checkAccessTitle = 'Access Denied',
  checkAccessMessage = 'You have no access to this page',
  checkExists,
  checkExistsTitle,
  checkExistsMessage,
  useQuery,
  setProps,
  Page,
}: PageWrapperProps<TProps, TQueryResult>) => {
  const navigate = useNavigate()
  const ctx = useAppContext()
  const queryResult = useQuery?.()

  const redirectNeeded = redirectAuthorized && ctx.me

  useEffect(() => {
    if (redirectNeeded) {
      navigate(getAllIdeasRoute(), { replace: true })
    }
  }, [redirectNeeded, navigate])

  if (queryResult?.isLoading || queryResult?.isFetching || redirectNeeded) {
    return <p>Loading...</p>
  }

  if (queryResult?.isError) {
    return <ErrorPageComponent message={queryResult.error.message} />
  }

  if (authorizedOnly && !ctx.me) {
    return <ErrorPageComponent title={authorizedOnlyTitle} message={authorizedOnlyMessage} />
  }

  const helperProps = { ctx, queryResult: queryResult as never }

  if (checkAccess) {
    const accessDenied = !checkAccess(helperProps)
    if (accessDenied) {
      return <ErrorPageComponent title={checkAccessTitle} message={checkAccessMessage} />
    }
  }

  if (checkExists) {
    const notExists = !checkExists(helperProps)
    if (notExists) {
      return <NotFoundPage title={checkExistsTitle} message={checkExistsMessage} />
    }
  }

  try {
    const props = setProps?.({ ...helperProps, checkExists: checkExistsFn, checkAccess: checkAccessFn }) as TProps
    return <Page {...props} />
  } catch (error) {
    if (error instanceof CheckExistsError) {
      return <NotFoundPage title={checkExistsTitle} message={error.message || checkExistsMessage} />
    }
    if (error instanceof CheckAccessError) {
      return <ErrorPageComponent title={checkAccessTitle} message={error.message || checkAccessMessage} />
    }
    throw error
  }
}

// Добавляем displayName
PageWrapper.displayName = 'PageWrapper'

export const withPageWrapper = <TProps extends BaseProps = {}, TQueryResult extends QueryResult<any> | undefined = undefined>(
  pageWrapperProps: Omit<PageWrapperProps<TProps, TQueryResult>, 'Page'>
) => {
  return (Page: PageWrapperProps<TProps, TQueryResult>['Page']) => {
    return () => <PageWrapper {...pageWrapperProps} Page={Page} />
  }
}

Рекомендации по избеганию этих ошибок

1. Профилактика ошибок no-explicit-any

  • Используйте unknown для внешних данных - данные из API, форм, localStorage должны быть типа unknown
  • Создавайте типы для API ответов - определите интерфейсы для всех API вызовов
  • Используйте утилиты типа - Partial, Pick, Omit для комбинирования типов
  • Включите strict mode в tsconfig.json - это автоматически включает noImplicitAny

2. Профилактика ошибок no-empty-object-type

  • Используйте Record<string, never> когда вам нужен объект без свойств
  • Предпочитайте object когда вам нужен любой объектный тип
  • Создавайте интерфейсы для сложных структур данных

3. Профилактика ошибок missing display name

  • Добавляйте displayName сразу после создания компонента
  • Используйте ESLint плагин с автоматическим исправлением
  • Рассмотрите использование библиотек вроде babel-plugin-transform-react-display-name

Конфигурация ESLint

Для предотвращения этих ошибок добавьте в ваш .eslintrc:

json
{
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended"
  ],
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-empty-object-type": "error",
    "react/display-name": "error"
  }
}

Источники

  1. no-explicit-any | typescript-eslint
  2. no-empty-object-type | typescript-eslint
  3. Avoiding anys with Linting and TypeScript | typescript-eslint
  4. Component definition is missing displayName (react/display-name) | Stack Overflow
  5. no-missing-component-display-name | ESLint React
  6. eslint-plugin-react/docs/rules/display-name.md

Заключение

Исправление этих трех типов ESLint ошибок значительно улучшит качество вашего TypeScript кода:

  1. Замена any на конкретные типы делает код более типобезопасным и улучшает автодополнение IDE
  2. Использование {} вместо Record<string, never> или object предотвращает скрытые ошибки в типах
  3. Добавление displayName улучшает отладку и разработку компонентов

Следуя этим рекомендациям, вы создадите более надежный и поддерживаемый код React-приложения с TypeScript. Начните с малого - добавьте displayName компонента, затем замените any на конкретные типы, и в конце разберитесь с пустыми объектными типами.

Авторы
Проверено модерацией
Модерация