React TypeScript Component Types: Props & State Guide
React components are JavaScript functions that accept props and return JSX. When you add TypeScript, you declare the shape of props with an interface, explicitly type state and hooks, and ensure event handlers match their targets. This is where TypeScript becomes powerful in React: every component becomes a contract, and breaking that contract fails to compile.
How Do You Type Props in a React Component?
Props are the way parent components pass data to child components. In TypeScript, you define the shape of props with an interface, then annotate your component function to accept that interface. This is the most fundamental pattern in typed React.
Here is a simple example:
// Define the shape of props using an interface
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
// Type the component function with the interface
function Button({ label, onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// Parent uses the component; TypeScript checks props
export function App() {
return (
<>
<Button label="Click me" onClick={() => console.log("clicked")} />
<Button label="Save" onClick={() => {}} disabled={true} />
{/* ERROR: 'label' prop is required (not optional) */}
<Button onClick={() => {}} />
</>
);
}
The disabled?: boolean syntax makes a prop optional (it may or may not be provided). Without the ?, a prop is required. TypeScript enforces this at the call site—if you forget a required prop, the build fails.
What Is the Difference Between Interface and Type for Props?
Both interface and type can describe props, but conventions differ. For React component props, most teams prefer interface because it is explicit and familiar, though type works identically.
Here is the same component using both approaches:
// Using interface (preferred for props)
interface CardProps {
title: string;
description: string;
}
function Card(props: CardProps) {
return <div><h2>{props.title}</h2><p>{props.description}</p></div>;
}
// Using type (also valid)
type CardProps = {
title: string;
description: string;
};
function Card(props: CardProps) {
return <div><h2>{props.title}</h2><p>{props.description}</p></div>;
}
In practice, use interface for component props and type for single-value types or unions. We cover the distinction more deeply in a later article.
How Do You Type useState in React?
The useState hook creates state. When you call useState, TypeScript infers the state type from the initial value, but you can also pass an explicit generic type parameter for clarity or when the initial value is null:
import { useState } from 'react';
function Counter() {
// Type inferred as number from initial value 0
const [count, setCount] = useState(0);
// TypeScript knows count is number, setCount expects number
// Type inferred as string from initial value
const [name, setName] = useState("Alice");
// Explicit type annotation for nullable state
const [user, setUser] = useState<string | null>(null);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Name: {name}</p>
<input value={name} onChange={(e) => setName(e.currentTarget.value)} />
</div>
);
}
When state can start as null (e.g., fetching a user), always provide the type explicitly:
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
// Without the explicit type, TypeScript infers | null is impossible
const [user, setUser] = useState<User | null>(null);
// Now TypeScript knows user is User | null
const displayName = user ? user.name : "Loading...";
return <div>{displayName}</div>;
}
How Do You Type Event Handlers in React?
Event handlers receive an event object from the browser. In TypeScript, you must type the event to use its properties safely. The most common pattern is to use React's built-in event types like React.ChangeEvent or React.FormEvent:
import { ChangeEvent, FormEvent } from 'react';
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// Type the input change event
const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
setEmail(event.currentTarget.value);
};
// Alternatively, you can inline the type
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.currentTarget.value);
};
// Type the form submit event
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(`Logging in: ${email}`);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={handleEmailChange}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={handlePasswordChange}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
The event types come from React's type definitions. For input elements, use ChangeEvent<HTMLInputElement>. For buttons and links, use MouseEvent<HTMLButtonElement>. For forms, use FormEvent<HTMLFormElement>. TypeScript's autocomplete shows available event types as you type.
How Do You Type useEffect and Async Operations?
The useEffect hook runs side effects (like fetching data) after render. When fetching, you typically fetch in an async function inside the effect, not by making the effect itself async:
import { useState, useEffect } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
function BlogPost({ postId }: { postId: number }) {
const [post, setPost] = useState<Post | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Define an async function inside the effect
const fetchPost = async () => {
try {
const response = await fetch(`/api/posts/${postId}`);
const data: Post = await response.json();
setPost(data);
setLoading(false);
} catch (err) {
setError((err as Error).message);
setLoading(false);
}
};
fetchPost();
}, [postId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!post) return <div>No post found</div>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
Note: TypeScript infers the dependency array's types automatically, but you must ensure that dependencies that change frequently are listed to avoid stale closures.
What About Optional Props and Default Values?
React components often have optional props with sensible defaults. TypeScript supports both syntaxes:
interface AlertProps {
message: string;
severity?: "info" | "warning" | "error"; // Optional prop
timeout?: number;
}
// Use defaults in the function signature
function Alert({
message,
severity = "info",
timeout = 5000,
}: AlertProps) {
const colors = {
info: "blue",
warning: "orange",
error: "red",
};
return (
<div style={{ color: colors[severity] }}>
{message}
</div>
);
}
// Parent component
export function App() {
return (
<>
<Alert message="Update successful" />
<Alert message="Warning!" severity="warning" timeout={3000} />
</>
);
}
The ? makes a prop optional in the interface; the = defaultValue provides the default when the prop is not passed. TypeScript checks both the interface definition and the actual usage.
Key Takeaways
- Define component props with an
interfaceand annotate the component function with that interface. - Use
useState<Type>to explicitly type state, especially for nullable state likeuseState<User | null>(null). - Type event handlers with React event types like
ChangeEvent<HTMLInputElement>to access event properties safely. - Never make
useEffectasync directly; define an async function inside the effect and call it. - Mark props as optional with
?in the interface; provide default values in the function signature.
Frequently Asked Questions
Do I need to type the children prop?
If your component accepts children (elements passed between the opening and closing tags), use children?: ReactNode or children?: React.ReactNode in the props interface. The ReactNode type includes components, elements, text, arrays, and fragments.
interface ContainerProps {
children?: React.ReactNode;
}
function Container({ children }: ContainerProps) {
return <div className="container">{children}</div>;
}
How do I type a component that renders different things based on props?
Use discriminated unions or function overloads. The simplest approach is a union type where different props lead to different outputs. We cover this pattern in depth in the article on union types.
What is React.FC and should I use it?
React.FC (FunctionComponent) is an older pattern that pre-types a function as a React component. Modern React code does not use it; instead, just type the props and let TypeScript infer the return type. React.FC adds unnecessary boilerplate:
// Modern approach (preferred)
function Button({ label }: ButtonProps) {
return <button>{label}</button>;
}
// Older approach (avoid)
const Button: React.FC<ButtonProps> = ({ label }) => {
return <button>{label}</button>;
};
How do I type a component that accepts a ref?
Use forwardRef and the Ref type. This is an advanced topic covered in a later article.