TypeScript adds static typing to JavaScript, allowing developers to define the types of variables, function parameters, and return values. When working with React components in TypeScript, we can define the types of props and state to ensure that the component receives the correct data and behaves as expected.
Props are used to pass data from a parent component to a child component. In TypeScript, we can define the types of props using interfaces or types.
State is used to manage the internal data of a component. Similar to props, we can define the types of state in TypeScript.
Function components are the recommended way to write React components in modern React applications. In TypeScript, we can define the types of props for a function component using an interface or a type.
import React from 'react';
// Define the props interface
interface HelloProps {
name: string;
age?: number;
}
// Define the function component
const Hello: React.FC<HelloProps> = ({ name, age }) => {
return (
<div>
<p>Hello, {name}!</p>
{age && <p>You are {age} years old.</p>}
</div>
);
};
export default Hello;
In the above example, we define an interface HelloProps
to specify the types of props for the Hello
component. The name
prop is required, while the age
prop is optional.
Class components are another way to write React components. In TypeScript, we can define the types of props and state for a class component using generics.
import React, { Component } from 'react';
// Define the props interface
interface CounterProps {
initialCount: number;
}
// Define the state interface
interface CounterState {
count: number;
}
// Define the class component
class Counter extends Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = {
count: props.initialCount,
};
}
increment = () => {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default Counter;
In the above example, we define an interface CounterProps
to specify the types of props and an interface CounterState
to specify the types of state for the Counter
component.
Defining prop types is essential for ensuring that the component receives the correct data. We can use interfaces or types to define prop types.
import React from 'react';
// Define the props interface
interface UserProps {
name: string;
email: string;
isAdmin: boolean;
}
// Define the function component
const User: React.FC<UserProps> = ({ name, email, isAdmin }) => {
return (
<div>
<p>Name: {name}</p>
<p>Email: {email}</p>
<p>Admin: {isAdmin ? 'Yes' : 'No'}</p>
</div>
);
};
export default User;
We can define default values for props using the defaultProps
property.
import React from 'react';
// Define the props interface
interface MessageProps {
text: string;
color?: string;
}
// Define the function component
const Message: React.FC<MessageProps> = ({ text, color }) => {
return (
<p style={{ color }}>
{text}
</p>
);
};
// Define default props
Message.defaultProps = {
color: 'black',
};
export default Message;
Defining state types is important for ensuring that the component’s internal data is of the correct type. We can use interfaces or types to define state types.
import React, { Component } from 'react';
// Define the state interface
interface TodoState {
todos: string[];
newTodo: string;
}
// Define the class component
class TodoList extends Component<{}, TodoState> {
constructor(props: {}) {
super(props);
this.state = {
todos: [],
newTodo: '',
};
}
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
newTodo: e.target.value,
});
};
handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
this.setState((prevState) => ({
todos: [...prevState.todos, prevState.newTodo],
newTodo: '',
}));
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.newTodo}
onChange={this.handleChange}
/>
<button type="submit">Add Todo</button>
</form>
<ul>
{this.state.todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}
}
export default TodoList;
The React.FC
type has some limitations, such as not supporting defaultProps
and children
in the same way as a regular function component. It is recommended to use a regular function component type definition instead.
import React from 'react';
// Define the props interface
interface CardProps {
title: string;
children?: React.ReactNode;
}
// Define the function component
const Card = ({ title, children }: CardProps) => {
return (
<div>
<h2>{title}</h2>
{children}
</div>
);
};
export default Card;
It is a good practice to separate type definitions into their own files, especially for larger projects. This makes the code more modular and easier to maintain.
// types.ts
export interface ProductProps {
name: string;
price: number;
}
// Product.tsx
import React from 'react';
import { ProductProps } from './types';
const Product: React.FC<ProductProps> = ({ name, price }) => {
return (
<div>
<p>{name}</p>
<p>${price}</p>
</div>
);
};
export default Product;
TypeScript can often infer types automatically, so we don’t always need to explicitly define types. This can make the code more concise.
import React from 'react';
const Button = ({ text }: { text: string }) => {
return <button>{text}</button>;
};
export default Button;
Declaring React components with TypeScript can bring many benefits, such as type safety, better code readability, and improved maintainability. By understanding the fundamental concepts, usage methods, common practices, and best practices, developers can write more robust and reliable React applications. Remember to define prop types, state types, and use TypeScript’s features effectively to catch errors early in the development process.