⚛ Real World · Lesson 15

TypeScript
+ React

Type-safe components, props, state, hooks, and events. The most common real-world use of TypeScript.

Setting Up

Create a new TypeScript React project with Vite — the fastest way to get started.

terminalbash
# Create a new React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

This gives you a project with .tsx files (TypeScript + JSX), a tsconfig.json, and full type support out of the box.

Typing Component Props

Define a type or interface for your component's props to get full autocomplete and error checking.

Button.tsxTSX
// Define the shape of your props
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
}

function Button({ label, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
}

// Usage — TypeScript will error if you miss required props
<Button label="Save" onClick={() => save()} />             // ✅
<Button label="Delete" onClick={() => del()} variant="danger" /> // ✅
// <Button />  ❌ Missing required props: label, onClick

Typing useState

TypeScript usually infers the state type automatically — but sometimes you need to be explicit.

Counter.tsxTSX
import { useState } from 'react';

interface User {
  name: string;
  email: string;
}

function UserProfile() {
  // TypeScript infers: number
  const [count, setCount] = useState(0);

  // Explicit type — starts as null, will become User
  const [user, setUser] = useState<User | null>(null);

  // TypeScript infers: string[]
  const [items, setItems] = useState<string[]>([]);

  const loadUser = () => {
    setUser({ name: "Alice", email: "[email protected]" });
  };

  return <div>{user?.name ?? "Loading..."}</div>;
}

Typing useRef & useEffect

useRefTSX
import { useRef } from 'react';

function InputFocus() {
  // Type the DOM element
  const inputRef = useRef<HTMLInputElement>(null);

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

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focus}>
        Focus
      </button>
    </div>
  );
}
useEffectTSX
import { useEffect, useState } from 'react';

interface Post {
  id: number;
  title: string;
}

function Posts() {
  const [posts, setPosts] =
    useState<Post[]>([]);

  useEffect(() => {
    fetch('/api/posts')
      .then(r => r.json())
      .then((data: Post[]) => {
        setPosts(data);
      });
  }, []);

  return <ul>{posts.map(p =>
    <li key={p.id}>{p.title}</li>
  )}</ul>;
}

Typing Event Handlers

React exports event types for every HTML event. Use them to type your handlers precisely.

Form.tsxTSX
import { ChangeEvent, FormEvent, MouseEvent } from 'react';

function LoginForm() {
  // Input change event
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value); // ✅ TypeScript knows .value exists
  };

  // Form submit event
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // process form...
  };

  // Button click event
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log(e.clientX, e.clientY);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Login</button>
    </form>
  );
}

💡 Common React event types: ChangeEvent, FormEvent, MouseEvent, KeyboardEvent, FocusEvent, DragEvent — all from the 'react' package.

Typing Custom Hooks

useFetch.tsTS
import { useState, useEffect } from 'react';

interface FetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): FetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(url)
      .then(r => r.json())
      .then((d: T) => { setData(d); setLoading(false); })
      .catch(err => { setError(err.message); setLoading(false); });
  }, [url]);

  return { data, loading, error };
}

// Usage — T is inferred from the type annotation
interface User { id: number; name: string; }

const { data, loading } = useFetch<User>('/api/user/1');
// data is typed as User | null ✅

FC vs Function Declaration

// React.FC (older style)

old-way.tsxTSX
import { FC } from 'react';

const Card: FC<CardProps> = ({ title }) => {
  return <div>{title}</div>;
};

// Implicitly includes children prop
// Not recommended in modern React

// Modern approach ✅

new-way.tsxTSX
interface CardProps {
  title: string;
  children?: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return <div>{title}{children}</div>;
}

// Be explicit about children
// Preferred in React 18+

📌 Modern recommendation: Use plain function declarations with typed props interfaces. Avoid React.FC — it's more verbose and no longer provides advantages.

Previous
Lesson 14 — tsconfig.json
You are here
15 / 17
Next Lesson
Lesson 16 — TypeScript + Node.js