🛡️ React + TypeScript: Building Unbreakable UIs
JavaScript allows you to pass a string into a Prop expecting an Array, and it will silently render a massive error screen to your live users. By combining the Component Architecture of React with the Static Typing compiler of TypeScript, we can build mathematically bulletproof user interfaces.
Every modern enterprise React codebase uses TypeScript (.tsx files instead of .jsx).
1️⃣ Typing React Props
Props are just a massive data object passed down to a child Component. We type them by declaring an interface exactly defining what the Component requires to function.
// 1. Define the exact contract for the Props
interface UserCardProps {
name: string;
age: number;
// Optional props use a question mark (?)
avatarUrl?: string;
}
// 2. Destructure the Props, and mathematically bind them to the Interface using a colon
function UserCard({ name, age, avatarUrl }: UserCardProps) {
return (
<div className="card">
{/* If avatarUrl is missing, fallback to a default image */}
<img src={avatarUrl || "/default-avatar.png"} alt={name} />
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
);
}
// 3. Usage inside a Parent Component:
export function App() {
return (
<div>
{/* ✅ Legal: Sends all required props perfectly */}
<UserCard name="Mwero" age={25} />
{/* ❌ TS ERROR: Property 'name' is missing! The code refuses to compile. */}
<UserCard age={30} />
{/* ❌ TS ERROR: Type 'string' is not assignable to type 'number'. */}
<UserCard name="Alice" age="Twenty" />
</div>
);
}
2️⃣ Typing useState
React is incredibly smart about Type Inference. If you write useState(0), React instantly infers the type as number. You don't need to type it explicitly.
But what if the state starts off empty, and gets filled with a complex object later? You must use a Generic <T> to tell React what the state will eventually hold.
interface UserData {
id: string;
email: string;
}
function ProfileLoader() {
// 1. Type inference (Infers boolean)
const [isLoading, setIsLoading] = useState(true);
// 2. Explicit Generics
// State is CURRENTLY null, but in the future, it is mathematically guaranteed to be a UserData object.
const [user, setUser] = useState<UserData | null>(null);
const loadData = () => {
// ✅ Legal Object Match
setUser({ id: "991A", email: "test@test.com" });
// ❌ TS ERROR: Property 'email' is missing in type '{ id: string; }'
setUser({ id: "882B" });
};
if (isLoading) return <p>Loading...</p>;
// 3. Safely Narrowing Nulls
// By the time it hits the <h1>, TS demands you confirm 'user' isn't null.
// We use the Optical Chaining operator (?) to safely reach inside.
return <h1>{user?.email}</h1>;
}
3️⃣ Typing React Events (onChange, onClick)
When you pass an event object (e) into an onChange function in raw JavaScript, you get zero autocomplete assistance when typing e.target.value.
TypeScript exports hundreds of highly specific standard Event types for DOM interactions.
import { useState, ChangeEvent, FormEvent } from 'react';
function SearchForm() {
const [query, setQuery] = useState("");
// 1. Typing an Input Box (ChangeEvent bounded to an <HTMLInputElement>)
// Now, typing `e.` instantly auto-completes with `target`, `value`, `name`, etc.
const handleTyping = (e: ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};
// 2. Typing a Form Submission (FormEvent bounded to an <HTMLFormElement>)
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("Searching for:", query);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={query} onChange={handleTyping} />
<button type="submit">Search</button>
</form>
);
}
4️⃣ Typing useRef (DOM Targeting)
When using a Ref to directly interact with a physical DOM element, you must tell TypeScript exactly what specific HTML tag the Ref will be glued to. Otherwise, TS won't know if the tag supports the .focus() or .play() methods.
import { useRef } from 'react';
function VideoPlayer() {
// 1. Explicitly bound the Ref generic <T> to the HTMLVideoElement type.
// It must start with 'null' because the DOM hasn't drawn the video tag yet.
const videoRef = useRef<HTMLVideoElement>(null);
const handlePlay = () => {
// 2. We use Optional Chaining (?) to tell TS:
// "I swear the current value is not null anymore, execute the 'play' method."
videoRef.current?.play();
};
return (
<div>
{/* The Ref perfectly attaches to the video tag */}
<video ref={videoRef} src="/cat-video.mp4" />
<button onClick={handlePlay}>Play Movie</button>
</div>
);
}
5️⃣ Passing Functions as Props
Often, you must pass a function downwards so a Child component can update the Parent's state. Typing a function prop requires defining its parameters and its return type (void).
interface ModalProps {
title: string;
// This prop expects a function that takes ZERO arguments and returns NOTHING.
onCloseClick: () => void;
// This prop expects a function that requires a specific String, and returns NOTHING.
onSaveClick: (saveData: string) => void;
}
function SettingsModal({ title, onCloseClick, onSaveClick }: ModalProps) {
return (
<div className="modal">
<h2>{title}</h2>
<button onClick={onCloseClick}>X</button>
<button onClick={() => onSaveClick("My Saved Data")}>Save</button>
</div>
);
}
💡 Top TypeScript/React Gotchas
- Children Props:
childrenis naturally typed asReact.ReactNode.interface LayoutProps { children: React.ReactNode; } - Type Casting: Never "force" a type with
as string. It completely disables TypeScript's safety mechanisms and re-introduces runtime bugs. Fix the core architecture instead.