Question
I am creating a React form that displays data based on an API response. In a TypeScript project created with Create React App, the following class component produces this error on this.state.value:
class App extends React.Component {
constructor(props: {}) {
super(props);
this.state = { value: "" };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event: any) {
this.setState({ value: event.target.value });
}
handleSubmit(event: any) {
alert("A name was submitted: " + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
Why does TypeScript say Property 'value' does not exist on type 'Readonly<{}>', and how should the component be typed so the form works correctly?
Short Answer
You will learn why an untyped React class component gets an empty state type in TypeScript, how to declare its state shape, and how to type form event handlers safely. By the end, you can build controlled form inputs with this.state.value without the Readonly<{}> error.
Concept
React class components are generic: React.Component<Props, State>. TypeScript needs to know both the props shape and the state shape.
When you write this:
class App extends React.Component {
// ...
}
no state type is supplied. React's default state type is effectively an empty object ({}). React also exposes state as Readonly<State> so component code cannot directly mutate it. Therefore, TypeScript sees:
this.state // Readonly<{}>
An empty object type has no known value property, so this.state.value is rejected.
The assignment in the constructor does not reliably define the component's TypeScript state type. Instead, explicitly declare the shape of state and provide it to React.Component.
interface AppState {
value: string;
}
class App extends <{}, > {
}
Mental Model
Think of a TypeScript type as a label on a storage box.
this.stateis the box holding the component's changing data.React.Component<{}, {}>labels that box as empty.- Trying to read
this.state.valueis like asking for a compartment namedvaluein a box labelled “contains no known compartments.” - Defining
AppStatechanges the label to say: “This box has avaluecompartment containing text.”
Readonly is a safety seal on the box. You may inspect its contents, but you must use React's setState delivery process to replace or update them.
Syntax and Examples
Declare interfaces (or type aliases) for props and state, then pass them to React.Component<Props, State>.
import React from "react";
interface AppProps {}
interface AppState {
value: string;
}
class App extends React.Component<AppProps, AppState> {
state: AppState = {
value: "",
};
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ value: event.target.value });
};
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
alert(`A name was submitted: `);
};
() {
(
);
}
}
;
Step by Step Execution
Use this small example:
state = { value: "" };
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ value: event.target.value });
};
Suppose the user types Ada.
- On the first key press, the browser changes the input's temporary value to
A. - React calls
handleChangeand provides an event. event.target.valueis"A".this.setState({ value: "A" })schedules a state update.- React renders again.
this.state.valueis now"A", so the input receivesvalue="A". - The same cycle repeats for
danda; state ends as{ value: "Ada" }.
Real World Use Cases
Typed class component state is useful whenever a component owns changing UI data, including:
- Login forms:
email,password, validation messages, and a submitting flag. - Search pages: query text, loading status, API results, and request errors.
- Profile editors: initial data fetched from an API and fields the user can change.
- Shopping carts: selected quantities, coupon code, and checkout status.
- Admin dashboards: filter values, selected records, and pagination state.
For API-driven forms, state commonly includes both user input and request state:
interface ProfileState {
name: string;
loading: boolean;
error: string | null;
}
Using explicit types makes it clear which values may be absent, which are strings, and which states the UI must handle.
Real Codebase Usage
In production code, class component state is usually typed as a named interface and initialized as a class field. This keeps the component contract close to its implementation.
interface SearchState {
query: string;
loading: boolean;
error: string | null;
}
class SearchForm extends React.Component<{}, SearchState> {
state: SearchState = {
query: "",
loading: false,
error: null,
};
handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const query = this.state.query.trim();
if (!query) {
this.setState({ error: "Enter a search term." });
return;
}
this.({ : , : });
{
} {
.({ : });
} {
.({ : });
}
};
}
Common Mistakes
Omitting the state generic
class App extends React.Component {
state = { value: "" };
}
This can leave TypeScript with the default empty state type. Declare React.Component<Props, State>.
class App extends React.Component<{}, AppState> {
state: AppState = { value: "" };
}
Mutating state directly
// Incorrect
this.state.value = "New value";
Direct mutation bypasses React's update flow. Use:
this.setState({ value: "New value" });
Using any for events
Comparisons
| Approach | Best for | Key difference |
|---|---|---|
React.Component<{}, AppState> | Class components with no props | Explicitly defines the component state shape. |
React.Component<AppProps, AppState> | Class components receiving props | Types both incoming props and internal state. |
React.Component | Rarely appropriate in TypeScript | Defaults do not describe custom state fields. |
this.setState({ value: "x" }) | Updating independent fields | React merges the partial update into class state. |
this.setState(prev => ...) | Updates based on previous state | Avoids problems when updates are scheduled together. |
useState in function components |
Cheat Sheet
// 1. Describe props and state
interface Props {}
interface State {
value: string;
}
// 2. Supply the generic types
class App extends React.Component<Props, State> {
// 3. Initialize state
state: State = { value: "" };
// 4. Type an input change event
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ value: event.target.value });
};
// 5. Type a form submit event
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(this.state.);
};
}
FAQ
Why is React state shown as Readonly in TypeScript?
React types state as read-only to discourage direct mutation. Update it through setState, which lets React schedule rendering correctly.
Why does this.state = { value: "" } not fix the type error?
It creates a runtime value, but the class declaration still has the default state type unless you provide React.Component<Props, State> or another explicit state type.
What does {} mean in React.Component<{}, AppState>?
It means this component expects no props with known fields. It is the props type, not the state type.
Should I use object instead of {} for empty props?
Usually use a named empty interface such as interface AppProps {} for readability. In many codebases, Record<string, never> is used when you specifically want to prevent arbitrary props, but an empty props interface is sufficient for this example.
Do I need to type React events?
TypeScript can sometimes infer them in inline callbacks, but named handlers benefit from explicit types such as React.ChangeEvent<HTMLInputElement> and React.FormEvent<HTMLFormElement>.
Mini Project
Description
Build a typed name form that validates input before submission. It demonstrates a controlled text input, correctly typed class component state, typed events, and a simple success message.
Goal
Create a form that accepts a name, rejects blank submissions, and shows the submitted name.
Requirements
- Define a state type containing the input value, an error message, and a submitted name.
- Use a controlled text input connected to component state.
- Prevent the browser's default form submission.
- Show an error when the submitted name is empty or contains only spaces.
- Show a confirmation message after a valid submission.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.