Avatar ·

Ошибка "Property 'value' does not exist on type 'Readonly<{}>'" в React и TypeScript

Всем привет! Я новичок в TypeScript и React, и уже несколько часов бьюсь над ошибкой. У меня есть простой компонент, который должен получать значение из инпута и выводить его. Вот мой код: ```typescript import React, { useState } from 'react'; function MyComponent() { const [state, setState] = useState({}); const handleChange = (e: React.ChangeEvent) => { setState({ value: e.target.value }); }; return (

Value: {state.value}

); } ``` При попытке обратиться к `state.value` TypeScript выдаёт ошибку: **"Property 'value' does not exist on type 'Readonly<{}>'"** Я понимаю, что TypeScript говорит, что у объекта `{}` нет свойства `value`. Но я же его добавляю через `setState`! Почему TypeScript этого не видит? Как правильно типизировать состояние в таком случае? Я пробовал писать `state: any`, но это вроде не по-правильному. Подскажите, как исправить эту ошибку и как вообще правильно работать с useState в TypeScript, когда у объекта есть динамические поля? Буду благодарен за подробное объяснение! <<>>

Причина ошибки

Ошибка возникает из-за того, что вы инициализируете useState пустым объектом {}. TypeScript автоматически выводит тип этого состояния как Readonly<{}> — пустой объект, у которого нет никаких свойств. Когда вы пытаетесь обратиться к state.value, TypeScript справедливо сообщает, что такого свойства не существует.

Проблема не в том, что вы добавляете свойство через setState, а в том, что TypeScript проверяет типы статически (на этапе компиляции), а не во время выполнения. Он не знает, что вы планируете добавить туда свойство value позже.

Правильное решение: явное указание типа

Нужно явно указать тип состояния при вызове useState. Есть несколько способов:

  • Способ 1: Интерфейс для объекта (рекомендуется для сложных состояний)
  • Способ 2: Инлайн-тип (для простых случаев)

Способ 1: Определите интерфейс

import React, { useState } from 'react';

// Определяем интерфейс для состояния
interface MyState {
  value: string;
}

function MyComponent() {
  // Указываем тип явно
  const [state, setState] = useState<MyState>({ value: '' });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setState({ value: e.target.value });
  };

  return (
    <div>
      <input type="text" onChange={handleChange} />
      <p>Value: {state.value}</p>
    </div>
  );
}

Способ 2: Инлайн-тип (для простых случаев)

const [state, setState] = useState<{ value: string }>({ value: '' });

Важные моменты

  • Инициализация должна соответствовать типу: Если тип { value: string }, то начальное значение должно содержать это свойство. Нельзя писать useState<{ value: string }>({}) — это тоже вызовет ошибку.
  • Если свойство может быть необязательным: Используйте value?: string и проверяйте наличие.
  • Для нескольких полей: Добавляйте их в интерфейс и инициализируйте все сразу.

Альтернатива: использование примитива

Если вам нужно хранить только одно строковое значение, не используйте объект — храните строку напрямую:

const [value, setValue] = useState('');

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

return <p>Value: {value}</p>;

Это самый простой и правильный способ для данного случая. Тип string выводится автоматически из начального значения ''.

Почему это работает

Когда вы явно указываете тип useState<{ value: string }>({ value: '' }), TypeScript знает, что в состоянии есть свойство value типа string. При вызове setState он проверяет, что новый объект соответствует этому типу. При чтении state.value он знает, что это строка, и вы можете безопасно использовать её в JSX.

Это одна из ключевых особенностей TypeScript — статическая типизация помогает отлавливать ошибки на этапе разработки, а не во время выполнения.

<<>>

Использование Record для динамических ключей

Если вам действительно нужно, чтобы объект мог содержать произвольные ключи (не только value), используйте тип Record<string, any> или более точный вариант:

import React, { useState } from 'react';

function MyComponent() {
  const [state, setState] = useState<Record<string, string>>({});

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setState({ ...state, value: e.target.value });
  };

  return (
    <div>
      <input type="text" onChange={handleChange} />
      <p>Value: {state.value}</p>
    </div>
  );
}

Record<string, string> означает объект, где ключи — строки, а значения — тоже строки. TypeScript разрешит обратиться к state.value, но не будет знать конкретный тип значения (будет string | undefined).

Важно: Этот подход менее строгий, чем явное определение интерфейса. Используйте его только если количество и названия полей действительно неизвестны заранее. Для большинства случаев лучше определить конкретную структуру объекта.

Также можно использовать Partial<MyState>, если некоторые поля могут отсутствовать:

interface MyState {
  value: string;
  label?: string;
}

const [state, setState] = useState<Partial<MyState>>({});

В этом случае TypeScript будет ожидать, что свойства могут быть undefined, и вам придётся использовать state.value ?? '' или проверять наличие.