How To Use TypeScript React Tutorial Examples

Master type-safe React development with comprehensive examples covering components, hooks, and modern best practices

Why TypeScript with React?

Modern web development increasingly demands type safety and maintainable codebases. TypeScript has become the standard for building scalable React applications, offering compile-time error checking, improved IDE support, and clearer code documentation.

Whether you're building a small component library or a large-scale application, understanding how to effectively combine TypeScript with React will significantly improve your development experience and code quality. Our web development services team regularly leverages these patterns to build robust, maintainable applications for clients across various industries.

For projects requiring advanced functionality, our custom software development expertise ensures type-safe architectures that scale efficiently. Additionally, when building intelligent applications, combining TypeScript with React provides the foundation for seamless AI development integrations.

Setting Up TypeScript with React

Creating a New TypeScript React Project

The quickest way to start a React project with TypeScript is using Create React App with the TypeScript template:

npx create-react-app my-app --template typescript

This creates a new React application with TypeScript configured out of the box. Your component files now use the .tsx extension instead of .jsx, indicating TypeScript and JSX combined.

For Next.js projects, TypeScript support comes built-in. Create a new Next.js app and it will automatically detect and configure TypeScript.

Essential tsconfig.json Settings

Your tsconfig.json file controls TypeScript compilation. Key settings for React include:

{
 "compilerOptions": {
 "strict": true,
 "jsx": "react-jsx",
 "esModuleInterop": true,
 "moduleResolution": "bundler"
 }
}

The strict: true setting enables all strict type-checking options, recommended for modern React development.

For production applications, we recommend reviewing the official React TypeScript documentation for the most up-to-date guidance on type integration patterns.

Typing React Components

Function Components with Typed Props

Function components are the modern standard in React. Define an interface for your component's props:

interface ButtonProps {
 label: string;
 onClick: () => void;
 variant?: 'primary' | 'secondary' | 'danger';
 disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({
 label,
 onClick,
 variant = 'primary',
 disabled = false
}) => {
 return (
 <button
 className={`btn btn-${variant}`}
 onClick={onClick}
 disabled={disabled}
 >
 {label}
 </button>
 );
};

Using React.FC<ButtonProps> explicitly types the component and provides type safety. Optional props like variant and disabled are handled with default values.

Class Components with TypeScript

For legacy codebases, class components require typing for props, state, and lifecycle methods:

interface CounterState {
 count: number;
}

interface CounterProps {
 initialCount?: number;
}

class Counter extends React.Component<CounterProps, CounterState> {
 state: CounterState = {
 count: this.props.initialCount || 0
 };

 handleIncrement = () => {
 this.setState(prevState => ({
 count: prevState.count + 1
 }));
 };

 render() {
 return (
 <div>
 <p>Count: {this.state.count}</p>
 <button onClick={this.handleIncrement}>Increment</button>
 </div>
 );
 }
}

These patterns form the foundation of type-safe React development. Following best practices from SitePoint's comprehensive guide ensures consistent typing across your codebase.

Using TypeScript with React Hooks

useState with TypeScript

The useState hook works through type inference, but explicit typing is important for complex state:

// Simple state - TypeScript infers the type
const [count, setCount] = useState(0);
const [name, setName] = useState('');

// Complex state - Explicit typing recommended
interface User {
 id: number;
 name: string;
 email: string;
}

const [user, setUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);

// Union types for loading states
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<LoadingState>('idle');

useEffect with TypeScript

Proper typing of dependencies helps catch issues with stale closures:

useEffect(() => {
 const fetchData = async () => {
 const response = await fetch(`/api/users/${userId}`);
 const data = await response.json();
 setUsers(data);
 };

 fetchData();
}, [userId]);

useRef with TypeScript

Require explicit typing for DOM elements and mutable values:

// DOM ref with proper typing
const inputRef = useRef<HTMLInputElement>(null);

const focusInput = () => {
 inputRef.current?.focus();
};

// Mutable value ref
const counterRef = useRef<number>(0);

useReducer for Complex State

Pairs beautifully with discriminated unions for action types:

type Action =
 | { type: 'increment' }
 | { type: 'decrement' }
 | { type: 'set'; payload: number }
 | { type: 'reset' };

const reducer = (state: State, action: Action): State => {
 switch (action.type) {
 case 'increment':
 return { count: state.count + 1 };
 case 'set':
 return { count: action.payload };
 case 'reset':
 return { count: 0 };
 default:
 return state;
 }
};

const [state, dispatch] = useReducer(reducer, { count: 0 });

The useReducer pattern with discriminated unions provides excellent type safety for complex state logic, as documented in the TypeScript official handbook.

Event Handling and Form Types

Typing Form Events

Form handling requires proper typing of event objects:

interface FormData {
 name: string;
 email: string;
 message: string;
}

const ContactForm = () => {
 const [formData, setFormData] = useState<FormData>({
 name: '',
 email: '',
 message: ''
 });

 const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
 const { name, value } = e.target;
 setFormData(prev => ({ ...prev, [name]: value }));
 };

 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
 e.preventDefault();
 // Submit form data
 };

 return (
 <form onSubmit={handleSubmit}>
 <input name="name" value={formData.name} onChange={handleChange} />
 <input name="email" value={formData.email} onChange={handleChange} />
 <textarea name="message" value={formData.message} onChange={handleChange} />
 <button type="submit">Send</button>
 </form>
 );
};

Generic Components for Reusability

Create flexible, type-safe components that work with various data types:

interface ListProps<T> {
 items: T[];
 renderItem: (item: T, index: number) => React.ReactNode;
 keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
 return (
 <ul>
 {items.map((item, index) => (
 <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
 ))}
 </ul>
 );
}

// Usage with different types
const UserList = () => (
 <List<User>
 items={users}
 renderItem={(user) => <span>{user.name}</span>}
 keyExtractor={(user) => user.id.toString()}
 />
);

Generic components are essential for building reusable UI libraries. When developing comprehensive design systems, ensure all components follow these type-safe patterns for consistent developer experience.

Advanced Type Patterns

Utility Types for React

TypeScript provides powerful utility types for React development:

interface User {
 id: number;
 name: string;
 email: string;
 password: string;
}

// Partial - all properties optional
type PartialUser = Partial<User>;

// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Omit - exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;

Discriminated Unions for State Management

Discriminated unions provide excellent type safety for complex state:

type AsyncState<T> =
 | { status: 'idle' }
 | { status: 'loading' }
 | { status: 'success'; data: T }
 | { status: 'error'; error: Error };

if (state.status === 'success') {
 console.log(state.data.name); // TypeScript knows data is available
} else if (state.status === 'error') {
 console.log(state.error.message); // TypeScript knows error is available
}

This pattern eliminates bugs by ensuring you handle all possible states explicitly. According to 2025 TypeScript best practices, discriminated unions are among the most valuable patterns for production React applications.

For applications requiring robust state management, these patterns provide the foundation for reliable data flow and error handling.

Best Practices for 2025

Type Safety First

Never compromise on type safety. Avoid any except in specific scenarios. Use unknown with type guards:

// Good pattern
const processData = (data: unknown) => {
 if (typeof data === 'string') {
 return data.toUpperCase();
 }
 if (isUser(data)) {
 return data.name;
 }
 throw new Error('Unknown data type');
};

Embrace Type Inference

Let TypeScript work for you:

// TypeScript infers the type from array contents
const colors = ['red', 'green', 'blue'] as const;
// Type is: readonly ['red', 'green', 'blue']

Use Strict Mode

Always enable strict mode in tsconfig.json:

{
 "compilerOptions": {
 "strict": true,
 "noUnusedLocals": true,
 "noUnusedParameters": true,
 "noImplicitReturns": true
 }
}

These practices align with the official React TypeScript integration guidance and ensure your codebase remains maintainable as it grows. When building production applications, consider how these patterns integrate with your overall web development services strategy.

Frequently Asked Questions

Is TypeScript necessary for React?

No, TypeScript is not required for React development. However, it provides significant benefits including catch errors at compile time, improve code documentation, enhance IDE support, and make large codebases more maintainable. Many teams consider it essential for production applications.

What's the difference between TypeScript and PropTypes?

TypeScript provides compile-time type checking across your entire codebase, while PropTypes provides runtime validation. TypeScript catches errors during development and build time, whereas PropTypes only logs warnings in the browser console. TypeScript also offers better IDE integration and autocompletion.

How do I add TypeScript to an existing React project?

Install TypeScript and type definitions: npm install --save-dev typescript @types/react @types/react-dom. Rename your .jsx files to .tsx. Create a tsconfig.json file with appropriate compiler options. You may need to update your build configuration to handle TypeScript files.

What is the best way to handle nullable props?

Use the optional property syntax (?) in your interface for truly optional props. Use union types like string | null when the value can be explicitly null. Consider using default props or default parameter values to provide fallbacks. Always handle null cases in your component logic.

How does TypeScript improve React performance?

TypeScript itself doesn't directly improve runtime performance, but it helps prevent bugs that cause performance issues. Proper typing can help identify unnecessary re-renders, catch missing dependency arrays in useEffect, and ensure proper memoization with useCallback and useMemo. The main performance benefits come from better code quality and fewer bugs.

Ready to Build Type-Safe React Applications?

Our team of expert React developers specializes in building scalable, type-safe applications using TypeScript and modern React patterns.