Как исправить ошибки 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
Вот код компонента, вызывающий эти ошибки:
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”
- Исправление ошибки “empty object type {}”
- Исправление ошибки “missing display name”
- Полное решение для вашего компонента
- Рекомендации по избеганию этих ошибок
Исправление ошибки “Unexpected any”
Ошибка Unexpected any. Specify a different type возникает из-за использования типа any, который противоречит принципам строгой типизации TypeScript.
Почему это важно:
Как указано в документации typescript-eslint, тип any отключает проверку типов и может приводить к ошибкам времени выполнения. Правило no-explicit-any рекомендуется включать в проекты для поддержания качества кода.
Способы исправления:
- Используйте
unknownвместоany- это самый безопасный подход, требующий явного приведения типов - Определяйте конкретные типы - создайте интерфейсы или типы для данных, с которыми работаете
- Используйте дженерики - как в вашем коде, для обобщенных функций и компонентов
Пример исправления:
// Было:
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, что делает его слишком широким.
Способы исправления:
- Используйте
Record<string, never>- явно указывает, что объект не должен иметь свойств - Используйте
object- базовый тип для всех не-null значений, не являющихся примитивами - Определите конкретный интерфейс - когда вы точно знаете, какие свойства должны быть
Пример исправления:
// Было:
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
Способы исправления:
- Добавить
displayNameявно - присвоить строку переменной компонента - Использовать
React.memoс именем - для мемоизированных компонентов - Использовать HOC с displayName - для компонентов оберток
Пример исправления:
// Было:
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'
Полное решение для вашего компонента
Вот исправленная версия вашего кода с решением всех трех ошибок:
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:
{
"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"
}
}
Источники
- no-explicit-any | typescript-eslint
- no-empty-object-type | typescript-eslint
- Avoiding
anys with Linting and TypeScript | typescript-eslint - Component definition is missing displayName (react/display-name) | Stack Overflow
- no-missing-component-display-name | ESLint React
- eslint-plugin-react/docs/rules/display-name.md
Заключение
Исправление этих трех типов ESLint ошибок значительно улучшит качество вашего TypeScript кода:
- Замена
anyна конкретные типы делает код более типобезопасным и улучшает автодополнение IDE - Использование
{}вместоRecord<string, never>илиobjectпредотвращает скрытые ошибки в типах - Добавление
displayNameулучшает отладку и разработку компонентов
Следуя этим рекомендациям, вы создадите более надежный и поддерживаемый код React-приложения с TypeScript. Начните с малого - добавьте displayName компонента, затем замените any на конкретные типы, и в конце разберитесь с пустыми объектными типами.