TL;DR
Type React components, props, hooks, events, and context with TypeScript. Catch UI bugs at compile time and build self-documenting code.
Key concepts
- React TypeScript
- TypeScript React props
- typed hooks
- React TypeScript tutorial
React and TypeScript
TypeScript and React work together to catch UI bugs at compile time. Typed props, hooks, and events make components self-documenting and refactor-safe.
Typing Props
Define component props with interfaces for type-safe component APIs.
// Props interfaces
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
interface CardProps {
title: string;
children: string; // simplified — in React this would be ReactNode
footer?: string;
}
// Simulate component rendering
function Button(props: ButtonProps): string {
const { label, variant = "primary", disabled = false } = props;
const state = disabled ? " (disabled)" : "";
return `<button class="${variant}"${state}>${label}</button>`;
}
function Card(props: CardProps): string {
const { title, children, footer } = props;
let html = `<div class="card"><h2>${title}</h2><div>${children}</div>`;
if (footer) html += `<footer>${footer}</footer>`;
html += `</div>`;
return html;
}
console.log(Button({ label: "Submit", onClick: () => {}, variant: "primary" }));
console.log(Button({ label: "Delete", onClick: () => {}, variant: "danger", disabled: true }));
console.log(Card({ title: "Welcome", children: "Hello, TypeScript!" }));
console.log(Card({ title: "Info", children: "With footer", footer: "Last updated today" }));
Typing Hooks
Type useState, useReducer, and custom hooks for safe state management.
// Simulate React hooks for demonstration
// useState — type is inferred or explicit
function useState<T>(initial: T): [T, (value: T | ((prev: T) => T)) => T] {
let state = initial;
const setState = (value: T | ((prev: T) => T)): T => {
state = typeof value === "function" ? (value as (prev: T) => T)(state) : value;
return state;
};
return [state, setState];
}
// Basic usage — type inferred
const [count, setCount] = useState(0);
console.log(`Count: ${count}`);
console.log(`After increment: ${setCount(prev => prev + 1)}`);
// Explicit type for complex state
interface FormState {
name: string;
email: string;
errors: Record<string, string>;
}
const [form, setForm] = useState<FormState>({
name: "",
email: "",
errors: {}
});
console.log(`Form: ${JSON.stringify(form)}`);
// useReducer pattern
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset"; payload: number };
interface CounterState {
value: number;
}
function reducer(state: CounterState, action: Action): CounterState {
switch (action.type) {
case "increment": return { value: state.value + 1 };
case "decrement": return { value: state.value - 1 };
case "reset": return { value: action.payload };
}
}
let state: CounterState = { value: 10 };
state = reducer(state, { type: "increment" });
console.log(`After increment: ${state.value}`);
state = reducer(state, { type: "decrement" });
console.log(`After decrement: ${state.value}`);
state = reducer(state, { type: "reset", payload: 0 });
console.log(`After reset: ${state.value}`);
Event Handling
Type event handlers to catch errors in event handling code.
// Simulated event types (matching React's type system)
interface ChangeEvent {
target: { value: string; name: string };
}
interface SubmitEvent {
preventDefault: () => void;
}
interface KeyboardEvent {
key: string;
ctrlKey: boolean;
}
// Typed event handlers
function handleChange(event: ChangeEvent): void {
console.log(`Field "${event.target.name}" changed to: ${event.target.value}`);
}
function handleSubmit(event: SubmitEvent): void {
event.preventDefault();
console.log("Form submitted");
}
function handleKeyDown(event: KeyboardEvent): void {
if (event.ctrlKey && event.key === "s") {
console.log("Save shortcut detected");
} else {
console.log(`Key pressed: ${event.key}`);
}
}
// Test the handlers
handleChange({ target: { value: "Alice", name: "username" } });
handleChange({ target: { value: "alice@example.com", name: "email" } });
handleSubmit({ preventDefault: () => console.log(" (default prevented)") });
handleKeyDown({ key: "s", ctrlKey: true });
handleKeyDown({ key: "Enter", ctrlKey: false });
Generic Components
Create reusable components that work with any data type.
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => string;
emptyMessage?: string;
}
function List<T>(props: ListProps<T>): string {
const { items, renderItem, emptyMessage = "No items" } = props;
if (items.length === 0) return `<p>${emptyMessage}</p>`;
const rendered = items.map((item, i) => `<li>${renderItem(item, i)}</li>`);
return `<ul>${rendered.join("")}</ul>`;
}
// Use with different types
interface User { name: string; role: string }
interface Product { name: string; price: number }
const users: User[] = [
{ name: "Alice", role: "Admin" },
{ name: "Bob", role: "User" }
];
const products: Product[] = [
{ name: "Laptop", price: 999 },
{ name: "Mouse", price: 29 }
];
console.log("Users:");
console.log(List({
items: users,
renderItem: (user) => `${user.name} (${user.role})`
}));
console.log("\nProducts:");
console.log(List({
items: products,
renderItem: (product) => `${product.name}: $${product.price}`
}));
console.log("\nEmpty:");
console.log(List<string>({
items: [],
renderItem: (s) => s,
emptyMessage: "Nothing to show"
}));
Context Typing
Type React context to ensure providers and consumers agree on the shape.
// Simulated context system
interface ThemeContext {
mode: "light" | "dark";
primary: string;
toggle: () => string;
}
interface AuthContext {
user: { name: string; role: string } | null;
login: (name: string) => void;
logout: () => void;
}
// Context "providers"
function createThemeContext(): ThemeContext {
let mode: "light" | "dark" = "light";
return {
mode,
primary: "#007bff",
toggle() {
mode = mode === "light" ? "dark" : "light";
this.mode = mode;
return mode;
}
};
}
function createAuthContext(): AuthContext {
let user: { name: string; role: string } | null = null;
return {
user,
login(name: string) {
user = { name, role: "user" };
this.user = user;
console.log(`Logged in as ${name}`);
},
logout() {
user = null;
this.user = null;
console.log("Logged out");
}
};
}
// "Components" consuming context
function Header(theme: ThemeContext, auth: AuthContext): string {
const greeting = auth.user ? `Welcome, ${auth.user.name}` : "Please log in";
return `[${theme.mode}] ${greeting}`;
}
const theme = createThemeContext();
const auth = createAuthContext();
console.log(Header(theme, auth));
auth.login("Alice");
console.log(Header(theme, auth));
console.log(`Theme toggled to: ${theme.toggle()}`);
console.log(Header(theme, auth));
auth.logout();
console.log(Header(theme, auth));
Try It Yourself
// Build a type-safe form hook
interface FieldConfig {
initial: string;
validate?: (value: string) => string | null;
}
type FormConfig = Record<string, FieldConfig>;
type FormValues<T extends FormConfig> = { [K in keyof T]: string };
type FormErrors<T extends FormConfig> = Partial<{ [K in keyof T]: string }>;
function useForm<T extends FormConfig>(config: T) {
const values = {} as FormValues<T>;
const errors = {} as FormErrors<T>;
// Initialize values
for (const key in config) {
values[key] = config[key].initial as FormValues<T>[typeof key];
}
function setValue<K extends keyof T>(field: K, value: string): void {
values[field] = value as FormValues<T>[K];
// Validate on change
const validator = config[field as string as keyof T]?.validate;
if (validator) {
const error = validator(value);
if (error) {
errors[field] = error as FormErrors<T>[K];
} else {
delete errors[field];
}
}
}
function validateAll(): boolean {
let valid = true;
for (const key in config) {
const validator = config[key].validate;
if (validator) {
const error = validator(values[key]);
if (error) {
errors[key as keyof T] = error as FormErrors<T>[keyof T];
valid = false;
}
}
}
return valid;
}
return { values, errors, setValue, validateAll };
}
// Usage
const form = useForm({
name: {
initial: "",
validate: (v) => v.length < 2 ? "Name must be at least 2 characters" : null
},
email: {
initial: "",
validate: (v) => !v.includes("@") ? "Must be a valid email" : null
},
age: {
initial: "",
validate: (v) => isNaN(Number(v)) ? "Must be a number" : null
}
});
// Test validation
form.setValue("name", "A");
console.log(`Name error: ${form.errors.name ?? "none"}`);
form.setValue("name", "Alice");
console.log(`Name error: ${form.errors.name ?? "none"}`);
form.setValue("email", "bad");
console.log(`Email error: ${form.errors.email ?? "none"}`);
form.setValue("email", "alice@example.com");
console.log(`Email error: ${form.errors.email ?? "none"}`);
console.log(`\nAll valid: ${form.validateAll()}`);
console.log(`Values: ${JSON.stringify(form.values)}`);
Key Takeaways
- Define props with interfaces — optional props use
?, defaults via destructuring - Type
useStateexplicitly when the type can't be inferred from the initial value - Use discriminated unions for reducer actions (each action has a unique
type) - Generic components (
List<T>) work with any data type while staying type-safe - Type event handlers to match React's event types (
ChangeEvent,FormEvent, etc.) - Context types ensure providers and consumers agree on the data shape
Pro Tip: When props get complex, split them into smaller interfaces and compose with intersection types:
type Props = LayoutProps & DataProps & EventProps. This keeps each concern focused and makes individual prop groups reusable across components.
Next Steps
You've typed React components with props, hooks, and events. Now it's time to push TypeScript's type system further with discriminated unions, template literal types, conditional types, and branded types.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.