TypeScript in React: The Complete Guide
This guide represents my journey of learning TypeScript in React, compiled from various sources including YouTube tutorials, documentation, and articles that I found particularly helpful. As I was learning these concepts, I organized them into this comprehensive guide to serve as both a reference for myself and a resource for others. Whether you're new to TypeScript or looking to enhance your React applications with type safety, this guide provides practical examples and explanations that helped me understand and implement TypeScript effectively.
Before we dive in, it's worth noting that TypeScript has become the de facto standard in modern React applications. Through my learning process, I've experienced firsthand how it helps catch errors during development, improves code documentation, and enhances developer experience through better tooling and autocompletion.
Basic TypeScript Concepts
Converting JSX to TSX
To start using TypeScript in a React project, you need to rename your .jsx
files to .tsx
. This tells TypeScript to treat your files as React components with TypeScript support.
Typing Variables
TypeScript can infer types from variable assignments, but you can also explicitly define types:
// Type inference (preferred when possible)
const url = "https://example.com"; // TypeScript infers this as string
// Explicit typing
const url: string = "https://example.com";
When you try to reassign a variable with a different type, TypeScript will give you an error:
let url = "https://example.com";
url = 42; // Error: Type 'number' is not assignable to type 'string'
Typing Functions
Function parameters should be typed to prevent errors:
function convertCurrency(amount: number, currency: string): string {
// Implementation here
return `${amount} ${currency}`;
}
// Using the function
convertCurrency(100, "USD"); // Works fine
convertCurrency("100", "USD"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
Typing React Components
Basic Component Typing
In React components, you typically need to type the props:
function Button({ backgroundColor, fontSize, pillShape }: {
backgroundColor: string;
fontSize: number;
pillShape: boolean;
}) {
return (
<button
className={`button ${pillShape ? "rounded-full" : ""}`}
style={{ backgroundColor, fontSize }}
>
Click me
</button>
);
}
Extracting Types
For cleaner code, extract prop types to a separate type definition:
type ButtonProps = {
backgroundColor: string;
fontSize: number;
pillShape?: boolean; // ? makes this prop optional
};
function Button({ backgroundColor, fontSize, pillShape }: ButtonProps) {
return (
<button
className={`button ${pillShape ? "rounded-full" : ""}`}
style={{ backgroundColor, fontSize }}
>
Click me
</button>
);
}
Union Types
Union types allow for more specific type definitions:
type Color = "red" | "blue" | "green";
type ButtonProps = {
backgroundColor: Color;
textColor: Color;
fontSize: number;
};
Working with Arrays and Objects
Array Types
// Array of numbers
const padding: number[] = [5, 10, 20, 5];
// Alternative syntax
const scores: Array<number> = [85, 90, 95];
Tuple Types
Tuples are arrays with fixed length and specific types:
// A tuple defining padding: [top, right, bottom, left]
const padding: [number, number, number, number] = [5, 10, 20, 5];
Styling with React.CSSProperties
For typing style objects, use React's built-in CSSProperties
type:
type ButtonProps = {
style: React.CSSProperties;
};
function Button({ style }: ButtonProps) {
return <button style={style}>Click me</button>;
}
// Usage
<Button style={{ backgroundColor: 'red', fontSize: 16 }} />
Record Type
For objects with specific key and value types:
type BorderRadiusProps = {
borderRadius: Record<string, number>;
};
// Usage
<Button borderRadius={{ topLeft: 5, topRight: 10, bottomRight: 15, bottomLeft: 5 }} />
Typing Event Handlers
Basic Event Handler Types
function Button({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click me</button>;
}
// With parameters
function Button({ onClick }: { onClick: (test: string) => number }) {
return <button onClick={() => onClick("test")}>Click me</button>;
}
Event Objects
React automatically types inline event handlers, but extracted functions need explicit typing:
// Inline (automatically typed)
<button onClick={(e) => console.log(e)}>Click me</button>
// Extracted (needs explicit typing)
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e);
};
<button onClick={handleClick}>Click me</button>
Children and Component Composition
Typing Children
type ButtonProps = {
children: React.ReactNode; // Can accept text, elements, or other valid React nodes
};
function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
// Usage
<Button>Click me</Button>
<Button><span>Click</span> me</Button>
JSX.Element vs ReactNode
JSX.Element
is more restrictive (only elements), while ReactNode
accepts text, numbers, elements, etc.:
type RestrictiveProps = {
children: React.JSX.Element; // Only accepts elements
};
// This works
<RestrictiveComponent><div>Hello</div></RestrictiveComponent>
// This would error
<RestrictiveComponent>Hello</RestrictiveComponent>
React Hooks with TypeScript
useState
TypeScript infers types from initial values, but you can be explicit:
// Type inference from initial value
const [count, setCount] = useState(0); // count is inferred as number
// Explicit typing
const [text, setText] = useState<string>(""); // Explicitly setting type as string
// With complex objects, explicit typing is recommended
type User = {
name: string;
age: number;
};
// When initial value is null
const [user, setUser] = useState<User | null>(null);
// Access with optional chaining to handle null
console.log(user?.name);
useRef
// For DOM elements
const buttonRef = useRef<HTMLButtonElement>(null);
// Usage
<button ref={buttonRef}>Click me</button>
Advanced TypeScript Techniques
"as const" Type Assertion
Makes arrays/objects more specific and readonly:
// Without as const
const buttonTextOptions = ["Click me", "Click me again", "Click me one more time"];
// Type is string[]
// With as const
const buttonTextOptions = ["Click me", "Click me again", "Click me one more time"] as const;
// Type is readonly ["Click me", "Click me again", "Click me one more time"]
// Benefits when mapping
buttonTextOptions.map(option => {
// option is the exact string literal, not just any string
return <Button key={option}>{option}</Button>
});
Omit Utility Type
Remove properties from an existing type:
type User = {
name: string;
sessionId: string;
};
type Guest = Omit<User, "name">;
// Result: { sessionId: string }
Type Assertion with "as"
// When you know more about the type than TypeScript does
const buttonColor = localStorage.getItem("buttonColor") as "red" | "blue" | "green";
Generics
Generics allow you to create reusable components and functions:
// Generic function
function convertToArray<T>(value: T): T[] {
return [value];
}
// Usage
const numberArray = convertToArray(5); // Type: number[]
const stringArray = convertToArray("hello"); // Type: string[]
// Generic component
type ButtonProps<T> = {
countValue: T;
countHistory: T[];
};
function Button<T>({ countValue, countHistory }: ButtonProps<T>) {
return (
<button>
Current: {String(countValue)}, History: {countHistory.join(', ')}
</button>
);
}
// Usage
<Button<number> countValue={5} countHistory={[1, 2, 3, 4, 5]} />
<Button<string> countValue="five" countHistory={["one", "two", "three", "four", "five"]} />
Best Practices and Organization
Organizing Types
Create a centralized place for shared types:
// types.ts
export type Color = "red" | "blue" | "green";
// In your component
import { Color } from './types';
// or with type import
import type { Color } from './types';
Using "unknown" Instead of "any"
// Fetching data
useEffect(() => {
const fetchData = async () => {
const response = await fetch("https://api.example.com/data");
const data: unknown = await response.json();
// You must verify the shape before using it
if (isValidData(data)) {
// Now it's safe to use
console.log(data.name);
}
};
fetchData();
}, []);
// Type guard function
function isValidData(data: unknown): data is { name: string } {
return typeof data === 'object' && data !== null && 'name' in data;
}
Configuration
tsconfig.json
Key options to consider:
{
"compilerOptions": {
"jsx": "preserve", // How JSX should be transformed
"strict": true, // Enable all strict type-checking options
"esModuleInterop": true, // For import compatibility
"lib": ["dom", "dom.iterable", "esnext"] // Libraries to include
},
"include": ["src/**/*"] // Files to include
}
Working with Third-Party Libraries
Most popular libraries have TypeScript definitions available from DefinitelyTyped (@types):
npm install @types/react @types/react-dom
If a library doesn't have types, you can create your own declaration file:
// custom-library.d.ts
declare module 'custom-library' {
export function doSomething(value: string): number;
}
Conclusion
TypeScript brings significant benefits to React development, including:
- Catching errors during development
- Better autocompletion and IntelliSense
- More maintainable code with explicit contracts
- Better refactoring support
While there is a learning curve, the improved developer experience and code quality make it worthwhile for React projects of all sizes. Start with basic typing and gradually adopt more advanced features as you become comfortable with TypeScript.
Remember that TypeScript is designed to help you, not hinder you. When you run into challenges, consider whether the issue is highlighting a genuine problem in your code that would otherwise surface at runtime.
By following the examples in this guide, you'll be able to effectively use TypeScript in your React applications, creating more robust and maintainable code.